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:
- Standalone Address Import - Add addresses to existing entities (users, contacts, etc.)
- 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:
'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:
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 entityaddress_line_1- Street addresscountry_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/neighborhoodaddress_line_2- Apartment, suite, unit, etc.postal_code- Postal/ZIP code (or:zip,zipcode,postcode,postal)bldg- Building number/namebox- PO Boxico- In care ofadministrative_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 Column | Aliases |
|---|---|
entity_type | addressable_type |
entity_id | addressable_id |
address_line_1 | street, address, address1, address_1, line1 |
address_line_2 | address2, address_2, line2, apt, suite |
administrative_area | state, province, region |
locality | city, town |
dependent_locality | district, suburb, neighborhood, neighbourhood |
postal_code | zip, zipcode, postcode, postal |
country_code | country |
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:
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,USImport 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):
- Same entity (entity_type + entity_id)
- Same country_code
- Same postal_code (normalized: spaces/hyphens removed)
- Same address_line_1 (normalized: lowercase, trimmed, abbreviations standardized)
- Same address_line_2
- Same bldg, box, ico
Examples
Example 1: Enrichment (WILL update)
# 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 labelExample 2: Core Field Change (WILL NOT update - creates new)
# 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)
# 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
| Strategy | Use When |
|---|---|
| Skip duplicates | Importing addresses that might already exist, don't want duplicates |
| Update existing | Enriching existing addresses with coordinates, labels, or verification |
| Import anyway | Intentionally 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
- Export addresses
- Delete old addresses
- 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\UserSolution: 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 allowedSolution: Add the entity type to config/addresses.php under import.allowed_entity_types.
Address Required
Address line 1 is requiredSolution: 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
| Scenario | Use |
|---|---|
| Adding addresses to existing records (known IDs) | Standalone ImportAddressesAction |
| Ecommerce/CRM export with name + email, no IDs | Building blocks (custom import class) |
| Creating new entities and addresses together | Building blocks (custom import class) |
| Re-importing a package export | Standalone importer (round-trip) |
1. Use the ImportsAddresses Trait
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:
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:
firstOrCreatemeans 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
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
$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:
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:
| Field | Notes |
|---|---|
entity_type, entity_id | Polymorphic owner (accepted as-is on re-import) |
label | |
address_line_1, address_line_2 | |
administrative_area, locality, dependent_locality | Text values |
administrative_area_id, locality_id, dependent_locality_id | Subdivision ULIDs — include these for round-trip imports |
postal_code, country_code | |
lat, lon | |
is_verified, verification_provider, verified_at | |
is_primary | |
quality_score, quality_level | Computed at export time |
formatted_international, formatted_local | Computed 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:
- Check that
labelis the same (duplicates are scoped to entity + label) - Verify duplicate detection is enabled in config
- 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:
- Open
config/addresses.php - Add your entity class to the
import.allowed_entity_typesarray:
'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
],
],- Clear config cache:
php artisan config:clear- 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
- User Guide - General package features
- API Reference - Service layer documentation
- Configuration Guide - Config options