Skip to content

Import & Export Guide

Last updated: 2026-05-31

This guide covers importing addresses from CSV/Excel files and exporting address data.

Overview

The package provides two approaches for importing addresses:

  1. Standalone Address Import - Add addresses to existing entities (users, contacts, etc.)
  2. Building Blocks - Reusable components for creating custom entity+address imports

Standalone Address Import

Import addresses and attach them to existing entities in your database.

Setup

1. Configure Allowed Entity Types

In config/addresses.php:

php
'import' => [
    'allowed_entity_types' => [
        'App\Models\User',
        'App\Models\Contact',
        'App\Models\Company',
    ],
],

Only whitelisted entity types can receive imported addresses (security feature).

2. Add Import Action to Resource

In your AddressResource or custom resource:

php
use Viewflex\FilamentAddress\Filament\Actions\ImportAddressesAction;

public static function getHeaderActions(): array
{
    return [
        ImportAddressesAction::make(),
    ];
}

CSV Format

Required Columns

  • entity_type - Full class name (e.g., App\Models\User)
  • entity_id - Database ID of the entity
  • address_line_1 - Street address
  • country_code - ISO 3166-1 alpha-2 code (e.g., US, CA, GB)

Optional Columns

  • label - Address label (e.g., home, work, billing)
  • administrative_area - State/province (or: state, province, region)
  • locality - City/town (or use flexible aliases: city, town)
  • dependent_locality - District/neighborhood
  • address_line_2 - Apartment, suite, unit, etc.
  • postal_code - Postal/ZIP code (or: zip, zipcode, postcode, postal)
  • bldg - Building number/name
  • box - PO Box
  • ico - In care of
  • administrative_area_id - Subdivision ULID (see Round-trip Imports)
  • locality_id - Subdivision ULID (see Round-trip Imports)
  • dependent_locality_id - Subdivision ULID (see Round-trip Imports)

Flexible Column Mapping

The import system automatically recognizes common column name variations:

Standard ColumnAliases
entity_typeaddressable_type
entity_idaddressable_id
address_line_1street, address, address1, address_1, line1
address_line_2address2, address_2, line2, apt, suite
administrative_areastate, province, region
localitycity, town
dependent_localitydistrict, suburb, neighborhood, neighbourhood
postal_codezip, zipcode, postcode, postal
country_codecountry

The addressable_type/addressable_id aliases mean a CSV exported directly from the package can be re-imported without any column renaming.

Example CSV

See packages/filament-address-pro/resources/examples/sample_address_import.csv:

csv
entity_type,entity_id,label,address_line_1,address_line_2,administrative_area,locality,dependent_locality,postal_code,country_code
App\Models\User,1,home,123 Main St,,DC,Washington,,20500,US
App\Models\User,1,work,456 Oak Ave,Suite 200,MA,Boston,,02101,US
App\Models\User,2,home,789 Elm Street,,NY,New York,,10001,US

Import Options

When clicking the "Import Addresses" action, you'll see these options:

1. File Upload

Upload a CSV or Excel file (.csv, .xls, .xlsx).

2. Verify Addresses During Import

  • Enabled: Uses verification provider (USPS, Google, etc.) to standardize addresses
  • Disabled (default): Imports addresses as-is
  • Cost: May incur API costs depending on provider

3. Geocode Addresses During Import

  • Enabled: Adds lat/lon coordinates using Google Geocoding API
  • Disabled (default): No coordinates added
  • Cost: ~$0.005 per address

Background Processing

Large imports are processed in the background automatically:

  • ≤ 50 rows without Verify/Geocode — runs synchronously (instant result)
  • > 50 rows or Verify/Geocode enabled — dispatched as a background job; you'll receive a notification when complete

Background imports process rows in chunks of 50 with per-chunk database transactions and automatic retry for transient failures. If a background import fails, re-upload the same file with "Skip duplicates" (the default) — rows that succeeded are skipped, and only the remaining rows are imported.

Queue worker required: Background imports need a running queue worker (php artisan queue:work). See FAQ — Bulk operations run but nothing happens.

4. Duplicate Handling

  • Skip duplicates (default): Won't import if address already exists for entity
  • Update existing: Enriches matching addresses with new data (see Understanding "Update Existing" below)
  • Import anyway: Creates duplicate address records (allows intentional duplicates)

Understanding "Update Existing"

The "Update Existing" duplicate handling strategy enriches matching addresses with new data. It does NOT change core address fields that would make it a different address.

What Gets Updated ✅

  • Labels & metadata: Tags, notes, custom fields
  • Coordinates: Adds lat/lon if missing in existing record
  • Verification status: Upgrades unverified → verified
  • Secondary fields: address_line_2, bldg, box, ico (if more complete)

What Does NOT Get Updated ❌

  • address_line_1 (primary identifying field)
  • postal_code (primary identifying field)
  • country_code (primary identifying field)

Why? Changing these core fields means it's a DIFFERENT address, not an update to the existing one.

How Duplicate Detection Works

Two addresses are considered duplicates if they match on ALL of these (after normalization):

  1. Same entity (entity_type + entity_id)
  2. Same country_code
  3. Same postal_code (normalized: spaces/hyphens removed)
  4. Same address_line_1 (normalized: lowercase, trimmed, abbreviations standardized)
  5. Same address_line_2
  6. Same bldg, box, ico

Examples

Example 1: Enrichment (WILL update)

csv
# Existing in database:
entity_id=1, address_line_1="123 Main St", postal_code="10001", lat=null, label=""

# Importing:
entity_id=1, address_line_1="123 Main St", postal_code="10001", lat=40.7128, label="Home"

# Result: ✅ Updated with coordinates and label

Example 2: Core Field Change (WILL NOT update - creates new)

csv
# Existing in database:
entity_id=1, address_line_1="123 Main St", postal_code="10001"

# Importing:
entity_id=1, address_line_1="123 Main Street", postal_code="10001"  # "St" vs "Street"

# Result: ❌ Creates NEW address (address_line_1 doesn't match exactly after normalization)

Example 3: Address Move (WILL NOT update - creates new)

csv
# Existing in database:
entity_id=1, address_line_1="123 Main St", postal_code="10001"

# Importing:
entity_id=1, address_line_1="456 Oak Ave", postal_code="10002"

# Result: ❌ Creates NEW address (different address entirely)

When to Use Each Strategy

StrategyUse When
Skip duplicatesImporting addresses that might already exist, don't want duplicates
Update existingEnriching existing addresses with coordinates, labels, or verification
Import anywayIntentionally creating duplicate addresses (e.g., multiple "work" addresses)

Need to Change Core Fields?

If you need to update address_line_1, postal_code, or country_code:

Option 1: Manual edit

  • Edit addresses in the UI (recommended for small changes)

Option 2: Delete and re-import

  1. Export addresses
  2. Delete old addresses
  3. Import with corrected data

Option 3: Database update

  • Use migrations or direct SQL (advanced users only)

How It Works

1. Field Normalization

CSV column headers are automatically mapped to database fields:

"street"   → "address_line_1"
"state"    → "administrative_area"
"city"     → "locality"
"district" → "dependent_locality"
"zip"      → "postal_code"

2. Fuzzy Subdivision Matching

State/province and city names are matched against your database:

  • Handles typos: "Masachusetts" → "Massachusetts"
  • Recognizes abbreviations: "MA" → "Massachusetts"
  • Matches local names: "東京都" → "Tokyo"
  • ISO codes: "US-CA" → "California"

If no match is found, the import is flagged with low confidence.

3. Data Quality

Each address is assigned a confidence score:

  • 100%: All fields matched perfectly
  • 50-99%: Partial matches (e.g., state matched but city didn't)
  • < 50%: Import fails, address is skipped

4. Round-trip Imports

When you export addresses using the package's export action and include the Administrative Area ID, Locality ID, and/or Dependent Locality ID fields, re-importing that CSV skips fuzzy matching entirely for those levels. The import validates the ULID against the reference data and uses it directly.

This works across all instances of the same package version because subdivision ULIDs are pre-defined constants in the package's seed data — a migrate:fresh --seed on any machine produces the exact same IDs.

If an ID is present but not found in the reference data (e.g. after a major version upgrade that reorganized subdivisions), the import falls back to fuzzy text matching using the text column.

To get a round-trip-safe export: in the Export dialog, select the three * ID (for re-import) fields alongside the standard address columns. You do not need to remove the text columns — they serve as the human-readable view and as the fallback.

5. Results

After import completes, you'll see:

  • Imported: Successfully added addresses
  • Skipped: Duplicates or low-quality data
  • Failed: Validation errors or missing entity

Error Handling

Common import errors:

Entity Not Found

entity_id: 999 not found for entity_type: App\Models\User

Solution: Verify entity exists in database before importing.

Invalid Country Code

Country code must be a 2-letter ISO code (e.g., US, GB, CA)

Solution: Use uppercase 2-letter ISO codes. See: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2

Entity Type Not Allowed

entity_type: App\Models\Invoice is not allowed

Solution: Add the entity type to config/addresses.php under import.allowed_entity_types.

Address Required

Address line 1 is required

Solution: Ensure address_line_1 (or alias like street, address) is provided.

Building Blocks for Custom Imports

The standalone importer requires entities to already exist (identified by entity_type + entity_id). When your source data identifies records by name or email — such as an ecommerce order export, CRM migration, or web signup CSV — use the building blocks to write a thin custom import class that handles entity lookup or creation, then delegates address processing to the package.

When to use building blocks vs. the standalone importer

ScenarioUse
Adding addresses to existing records (known IDs)Standalone ImportAddressesAction
Ecommerce/CRM export with name + email, no IDsBuilding blocks (custom import class)
Creating new entities and addresses togetherBuilding blocks (custom import class)
Re-importing a package exportStandalone importer (round-trip)

1. Use the ImportsAddresses Trait

php
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Viewflex\FilamentAddress\Concerns\ImportsAddresses;

class UserImport implements ToModel, WithHeadingRow
{
    use ImportsAddresses;

    public function model(array $row)
    {
        // Create user
        $user = User::create([
            'name' => $row['name'],
            'email' => $row['email'],
            'password' => Hash::make(Str::random(16)),
        ]);

        // Import address using package trait
        $this->importAddress($user, [
            'address_line_1' => $row['street'],
            'administrative_area' => $row['state'],
            'locality' => $row['city'],
            'dependent_locality' => $row['district'] ?? null,
            'postal_code' => $row['zip'],
            'country_code' => $row['country'] ?? 'US',
            'label' => 'home',
        ], [
            'verify' => true,
            'geocode' => true,
        ]);

        return $user;
    }
}

2. Lookup or Create by Email

For ecommerce exports, CRM migrations, or signup CSVs where records are identified by email rather than a database ID, use firstOrCreate to find or create the entity:

php
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Viewflex\FilamentAddress\Concerns\ImportsAddresses;

class CustomerImport implements ToModel, WithHeadingRow
{
    use ImportsAddresses;

    public function model(array $row)
    {
        if (empty($row['email'])) {
            return null;
        }

        // Find existing customer or create a new one
        $customer = Customer::firstOrCreate(
            ['email' => $row['email']],
            [
                'first_name' => $row['first_name'] ?? '',
                'last_name' => $row['last_name'] ?? '',
                'password' => Hash::make(Str::random(16)),
            ]
        );

        $result = $this->importAddress($customer, [
            'address_line_1' => $row['address'] ?? $row['street'] ?? null,
            'address_line_2' => $row['address2'] ?? null,
            'administrative_area' => $row['state'] ?? null,
            'locality' => $row['city'] ?? null,
            'dependent_locality' => $row['district'] ?? null,
            'postal_code' => $row['zip'] ?? $row['postal_code'] ?? null,
            'country_code' => $row['country'] ?? 'US',
            'label' => $row['address_type'] ?? 'home',
        ], [
            'duplicate_handling' => 'skip',
        ]);

        return $result->isSuccess() ? $customer : null;
    }
}

Note: firstOrCreate means re-running the same import file is safe — existing customers are found by email and their addresses are de-duplicated normally. Adjust the match key and field mapping to suit your source data.

3. Multiple Addresses Per Entity

php
public function model(array $row)
{
    $contact = Contact::create([
        'first_name' => $row['first_name'],
        'last_name' => $row['last_name'],
        'phone' => $row['phone'],
        'email' => $row['email'],
    ]);

    // Import work address
    $this->importAddress($contact, [
        'address_line_1' => $row['work_street'],
        'administrative_area' => $row['work_state'],
        'locality' => $row['work_city'],
        'dependent_locality' => $row['work_district'] ?? null,
        'postal_code' => $row['work_zip'],
        'country_code' => 'US',
        'label' => 'work',
    ]);

    // Import home address if provided
    if (!empty($row['home_street'])) {
        $this->importAddress($contact, [
            'address_line_1' => $row['home_street'],
            'administrative_area' => $row['home_state'],
            'locality' => $row['home_city'],
            'dependent_locality' => $row['home_district'] ?? null,
            'postal_code' => $row['home_zip'],
            'country_code' => 'US',
            'label' => 'home',
        ]);
    }

    return $contact;
}

4. Handle Import Results

php
$result = $this->importAddress($entity, $addressData, $options);

if ($result->isSuccess()) {
    $address = $result->getAddress();
    $confidence = $result->getConfidence();
    // Success
} elseif ($result->isSkipped()) {
    $reason = $result->getError(); // "Duplicate address"
    // Skipped
} else {
    $error = $result->getError(); // Validation error
    // Failed
}

5. Using AddressImportProcessor Directly

For advanced use cases:

php
use Viewflex\FilamentAddress\Services\Import\AddressImportProcessor;

$processor = app(AddressImportProcessor::class);

$result = $processor->process([
    'street' => '123 Main St',
    'city' => 'New York',
    'state' => 'NY',
    'zip' => '10001',
    'country' => 'US',
], [
    'verify' => true,
    'geocode' => true,
]);

if ($result->isSuccess()) {
    $data = $result->getData();
    // Contains normalized, validated, geocoded data
    // with matched subdivision IDs

    $address = Address::create($data);
}

Export

Export is available via two actions:

  • Export Results (header action) — exports all rows matching the current table filters
  • Export Addresses (bulk action) — exports only the selected rows

Both produce the same output format. Choose fields in the export dialog, then download as CSV or Excel.

Available fields:

FieldNotes
entity_type, entity_idPolymorphic owner (accepted as-is on re-import)
label
address_line_1, address_line_2
administrative_area, locality, dependent_localityText values
administrative_area_id, locality_id, dependent_locality_idSubdivision ULIDs — include these for round-trip imports
postal_code, country_code
lat, lon
is_verified, verification_provider, verified_at
is_primary
quality_score, quality_levelComputed at export time
formatted_international, formatted_localComputed at export time; not importable

Filtered exports: Apply table filters (country, verification status, etc.) before using Export Results to export only matching records.

API Costs

Geocoding

  • Google: ~$0.005 per address
  • Cache hit: $0.000 (free)

Verification

  • USPS: Free (US only, rate limited)
  • Google: ~$0.005 per address
  • Smarty: ~$0.004-0.008 per address

Cost Estimation

For 1,000 addresses with verification + geocoding:

  • No caching: ~$10.00
  • 80% cache hit rate: ~$2.00
  • Import without verification/geocoding: $0.00

Troubleshooting

Import Hangs or Times Out

  • Cause: Large file with verification/geocoding enabled
  • Solution: Disable verification/geocoding for initial import, then use bulk verify action

Low Match Confidence

Low confidence subdivision matching: Could not match locality: Springfiled
  • Cause: Typo in city name or non-standard abbreviation
  • Solution: Correct the data in your CSV before importing

Duplicate Addresses

If addresses are being imported as duplicates:

  1. Check that label is the same (duplicates are scoped to entity + label)
  2. Verify duplicate detection is enabled in config
  3. Check address normalization is working (case, spaces, abbreviations)

Import Shows "Failed: X" with No Details

Symptoms:

  • Import completes with "Imported: 2 | Failed: 4"
  • No error details provided
  • Silently fails for some entity types

Cause: Entity type not in whitelist

Solution:

  1. Open config/addresses.php
  2. Add your entity class to the import.allowed_entity_types array:
php
'import' => [
    'allowed_entity_types' => [
        'App\Models\User',
        'App\Models\Customer',     // ← Add your entity
        'App\Models\Location',     // ← Add your entity
        // ... add all models that should receive addresses
    ],
],
  1. Clear config cache:
bash
php artisan config:clear
  1. Retry the import

Why this happens: The whitelist is a security feature that prevents accidentally importing addresses to unintended models. Only explicitly allowed entity types can receive imported addresses.

"Update Existing" Creates New Addresses Instead of Updating

Symptoms:

  • Selected "Update existing" duplicate handling
  • Import creates new addresses instead of updating
  • End up with duplicate addresses

Cause: Core address fields (address_line_1, postal_code, country_code) were changed in CSV

Explanation: "Update Existing" only enriches matching addresses. It does NOT change core identifying fields. If you change address_line_1 (even "St" → "Street"), the system treats it as a DIFFERENT address and creates a new record.

Solution:

  • For enrichment (adding labels, coordinates): ✅ Use "Update Existing"
  • For changing core fields: ❌ Delete old and re-import, OR edit manually in UI

See: Understanding "Update Existing" above for detailed explanation.

See Also

Released under a commercial license.