Skip to content

Filament Address Pro - User Guide

Last updated: 2026-05-13

Complete guide to using Filament Address Pro in your Laravel application.


Table of Contents

  1. Getting Started
  2. Basic Usage
  3. Entering Addresses
  4. Advanced Usage
  5. International Addresses
  6. Address Verification
  7. Customization
  8. Best Practices
  9. Troubleshooting

Getting Started

Prerequisites

Before installing, ensure you have:

  • Laravel 11, 12, or 13 application
  • Filament 4.0 or 5.0 installed
  • Google Maps API key with these APIs enabled:
    • Geocoding API
    • Maps JavaScript API
    • Places API (for the address search field)
    • Address Validation API (optional, for verification)

Installation

See INSTALLATION.md for full instructions, including beta access, Composer authentication, CI/CD environments, and scenario-based setup (fresh app vs. existing Filament app). Then follow the Quick Start in the README to run the install wizard.


Basic Usage

Adding Address Management to a Model

The most common use case is adding address management to existing models like User, Customer, or Company.

Step 1: Add the Trait

php
// app/Models/Customer.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Viewflex\FilamentAddress\Concerns\HasAddresses;

class Customer extends Model
{
    use HasAddresses;

    // ... rest of your model
}

The HasAddresses trait provides:

  • addresses() - Relationship to all addresses
  • primaryAddress() - Get the primary address
  • verifiedAddresses() - Get only verified addresses
  • hasPrimaryAddress() - Check if has a primary address
  • hasVerifiedAddresses() - Check if has any verified addresses
  • setPrimaryAddress($address) - Set an address as primary

Step 2: Add the Relation Manager

php
// app/Filament/Resources/CustomerResource.php
namespace App\Filament\Resources;

use Viewflex\FilamentAddress\Filament\RelationManagers\AddressesRelationManager;

class CustomerResource extends Resource
{
    // ... other methods

    public static function getRelations(): array
    {
        return [
            AddressesRelationManager::class,
        ];
    }
}

That's it! You now have a full address management interface on your Customer resource.


Entering Addresses

How the Form Works

Country selection is always the first step. Choosing a country determines which fields appear, how they are labeled, and which subdivision dropdowns are shown. Changing country resets the address fields and clears verification state.

Once a country is selected, the form shows fields appropriate to that country's address format — a US address has State and ZIP Code; a Japanese address has Prefecture, City, and Ward; some countries have no subdivision fields at all.

Subdivision dropdowns (state, city, district) are populated from the 17,000+ subdivisions seeded with the package — no API call required. For certain countries (FR, DE, ES, IT, NL), entering a postal code auto-populates the subdivision fields. See International Addresses for depth on how subdivisions work.

A live formatted preview below the fields updates in real time as you fill them, in both international and domestic formats.

Input Modes

How you enter the street address depends on the configured input mode (ADDRESS_INPUT_MODE):

ModeWhat the form showsHow address entry works
autocompleteSearch box with map previewType to search; one selection fills all fields including coordinates. Marked verified automatically.
geocodingMap preview + Address Line 1Type in Address Line 1 and tab out — blur geocoding fills remaining fields and updates the map. Click Verify for provider check.
manualAddress Line 1 only (no search box)Fill every field by hand. Click Verify when done.
autoBest available based on API keys configuredBrowser key present → autocomplete; server key only → geocoding; no keys → manual.

In autocomplete mode, the Google Places search box and map preview appear at the top of the address section. The street address fields below are filled by the selection — you can edit them afterward if needed.

In geocoding mode, no search box appears. The map is shown (it updates when blur geocoding fills coordinates), but address entry is entirely through Address Line 1 — type or paste, tab out, and the remaining fields fill automatically. Then click Verify.

In manual mode, no API calls are made during entry. Verification is the only automated quality check.

Which mode should I use? See Verification Best Practices — For New Addresses for a comparison of the three flows and when each is appropriate.

Address Line Fields

address_line_1 and address_line_2 are the standard two street address lines — exactly what appears on a mailing label.

  • address_line_1 — the primary street address: house number, street name, and optionally a secondary unit designator appended after a comma: 135 S 18th St, Apt 4.
  • address_line_2 — an overflow line for secondary unit info, or the line a verification provider returned separately.

In geocoding and manual modes, when you type or paste into Address Line 1 and tab out, unit designators are automatically split into Address Line 2:

What you type in Address Line 1Address Line 1 after geocodingAddress Line 2
135 S 18th St, Apt 4135 S 18th StApt 4
100 Main St, Suite 200100 Main StSuite 200
135 S 18th St Unit 5B135 S 18th StUnit 5B
PO Box 1234PO Box 1234(empty)

Recognised unit keywords: Apt, Suite/Ste, Unit, Room/Rm, #, Bldg/Building.

In autocomplete mode, the Places selection populates these fields directly. The split above does not apply — the provider's line structure is used as-is.

Additional Fields (bldg, box, ico)

These are hidden in a collapsed Additional Fields section at the bottom of the form. They exist for structured data that has dedicated semantic meaning beyond a mailing line.

FieldLabelPurposeAuto-prefix in formatted output
bldgBuildingBuilding name or identifier ("Tower B", "Empire State Building")None — value used as-is
boxBoxPO Box numberPO Box (if not already present)
icoIn Care OfAddressee modifier (c/o recipient)c/o (if not already present)

For box and ico, enter just the identifying value — the label is added automatically. Type 1234 in Box and the formatted address shows PO Box 1234. For bldg, the value is used as-is — enter the full name or designator as it should appear (Tower B, Sony Building, Rockefeller Center).

Any filled additional fields appear as a separate line in the formatted address preview, inserted after address_line_2 (or after address_line_1 if there is no second line). If all three fields are empty, no extra line appears.

These fields are for deliberate, structured input. They are not populated automatically by geocoding or autocomplete. Both duplicate detection and quality scoring treat them as independent dimensions — a bldg difference between two otherwise identical addresses counts as a distinct address.

When to use which

ScenarioRecommended field
Apartment / suite / unitaddress_line_2 (populated automatically from line 1 in geocoding mode)
PO Box - typed directly into line 1Leave it in address_line_1 — geocoding handles it fine
PO Box - imported from a structured data sourcebox (cleaner formatting, structured storage)
Named building as part of the addressaddress_line_2 for casual entry; bldg when importing structured data
"In care of" recipientico

Why doesn't geocoding move "PO Box 1234" to the box field automatically?

Verification providers (USPS, Smarty, Google) only look at address_line_1 and address_line_2. If we silently moved content to a dedicated field, the provider would receive an incomplete address and the standardization dialog would show a confusing mismatch. The dedicated fields are for cases where the caller controls each field explicitly — imports and API calls.


Advanced Usage

Using Address Form in Custom Forms

You can embed the address form in any Filament form:

php
use Viewflex\FilamentAddress\Filament\Schemas\AddressForm;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;

public static function form(Form $form): Form
{
    return $form->schema([
        Section::make('Customer Information')
            ->schema([
                TextInput::make('name')->required(),
                TextInput::make('email')->email()->required(),
            ]),

        Section::make('Primary Address')
            ->schema(AddressForm::schema())
            ->columns(2)
            ->collapsible(),
    ]);
}

Using Address Form in Action Modals

You can embed the address form in any Filament Action modal, useful for "Add Address" buttons on pages, table rows, or anywhere you need the full form without navigating away.

Add the HandlesMapSelection trait and a $data property to your page or resource class, then spread AddressForm::schema() into the action's ->form():

php
use Viewflex\FilamentAddress\Concerns\HandlesMapSelection;
use Viewflex\FilamentAddress\Filament\Schemas\AddressForm;
use Filament\Actions\Action;

class CustomerProfile extends Page
{
    use HandlesMapSelection;

    // Required for Action forms with live updates (map selection, dynamic dropdowns)
    public array $data = [];

    protected function getActions(): array
    {
        return [
            Action::make('addAddress')
                ->label('Add Address')
                ->icon('heroicon-o-plus')
                ->modalWidth('5xl')
                ->form([
                    ...AddressForm::schema(),
                ])
                ->action(function (array $data) {
                    $this->record->addresses()->create($data);
                }),
        ];
    }
}

Note: HandlesMapSelection automatically detects whether it's inside a modal (mountedActions) or a page form, and routes map place selections to the correct form. No extra wiring is needed.


Accessing Address Data Programmatically

php
// Get all addresses
$customer = Customer::find(1);
$addresses = $customer->addresses;

// Get primary address
$primary = $customer->primaryAddress();

// Access address fields
if ($primary) {
    $street = $primary->address_line_1;
    $city = $primary->locality;
    $state = $primary->administrative_area;
    $zip = $primary->postal_code;
    $country = $primary->country_code;

    // Get coordinates
    $latitude = $primary->lat;
    $longitude = $primary->lon;

    // Check verification status
    if ($primary->is_verified) {
        $provider = $primary->verification_provider; // 'usps', 'google', etc.
        $verifiedAt = $primary->verified_at;
        $metadata = $primary->verification_metadata; // JSON data
    }
}

Working with Multiple Addresses

php
// Create a new address
$customer->addresses()->create([
    'country_code' => 'US',
    'address_line_1' => '1600 Pennsylvania Avenue NW',
    'locality' => 'Washington',
    'administrative_area' => 'District of Columbia',
    'postal_code' => '20500',
    'is_primary' => true,
]);

// Update primary address
$newPrimary = $customer->addresses()->first();
$customer->setPrimaryAddress($newPrimary);

// Delete an address
$address = $customer->addresses()->find(2);
$address->delete();

// Filter addresses
$verified = $customer->verifiedAddresses();
$unverified = $customer->addresses()->unverified()->get();

Using Geocoding Service Directly

For custom geocoding needs:

php
use Viewflex\FilamentAddress\Services\GeocodingService;

$geocoder = app(GeocodingService::class);

// Forward geocoding (address → coordinates)
$result = $geocoder->geocode(
    '1600 Amphitheatre Parkway, Mountain View, CA',
    'US'
);

if ($result['success']) {
    $lat = $result['lat'];               // 37.4220656
    $lon = $result['lon'];               // -122.0840897
    $formatted = $result['formatted_address'];
    $components = $result['components'];

    // Address components
    $street = $components['address_line_1'];
    $city = $components['city'];
    $state = $components['state'];
    $stateAbbr = $components['state_abbr'];
    $zip = $components['postal_code'];

    // Verification data (if available)
    if ($components['is_verified']) {
        $provider = $components['verification_provider'];
        $verifiedAt = $components['verified_at'];
    }
} else {
    $error = $result['error'];
}

// Reverse geocoding (coordinates → address)
$result = $geocoder->reverseGeocode(37.4220656, -122.0840897);

if ($result['success']) {
    $address = $result['formatted_address'];
    $components = $result['components'];
}

Blur Geocoding Configuration (Power-User Options)

Control automatic geocoding behavior when users tab out of the address field. These options help reduce API costs while maintaining good UX.

Basic Configuration

env
# Enable/disable blur geocoding
ADDRESS_BLUR_GEOCODING_ENABLED=true

# Minimum characters before geocoding (prevents incomplete addresses)
ADDRESS_BLUR_MIN_LENGTH=10

# Skip if address already verified with coordinates
ADDRESS_BLUR_SKIP_IF_VERIFIED=true

# Debounce delay in milliseconds (0 = immediate)
ADDRESS_BLUR_DEBOUNCE_MS=0

# Full-address content detection: confidence boost when address_line_1 looks complete at blur time
ADDRESS_BLUR_DETECT_PASTE=true

Power-User Options for API Cost Reduction

Option 1: Field Completeness Requirement

Determines which fields must be filled before blur geocoding triggers:

env
ADDRESS_BLUR_REQUIRE_COMPLETENESS=basic

Options:

  • any (default) - No requirements, geocode as soon as user tabs out
  • basic - Requires city OR postal code to be filled ⭐ Recommended for production
  • complete - Requires city AND state to be filled

Impact:

  • any → 0% reduction (baseline)
  • basic → ~30% API call reduction
  • complete → ~50% reduction (but may frustrate users)

Example workflow with basic:

User types: "123 Main Street" → [tabs out]
System checks: Is city OR postal filled?
  ✅ Yes → Geocode and fill remaining fields
  ❌ No → Skip geocoding, wait for more data

Option 2: Smart Detection Level

Uses confidence scoring to prevent geocoding when data quality is too low:

env
ADDRESS_BLUR_SMART_DETECTION=smart

Options:

  • basic (default) - Always geocode (no filtering)
  • smart - Only geocode if confidence >= 0.60 ⭐ Recommended for production
  • aggressive - Only geocode if confidence >= 0.80

Confidence scoring factors:

  • Has street number: +0.25
  • Token count >= 3: +0.15 (e.g., "123 Main Street" = 3 tokens)
  • Has city: +0.20
  • Has state: +0.20
  • Has postal code: +0.20
  • Full address detected (5+ tokens or comma): +0.10
  • Cap: 1.00 maximum

Examples:

php
// Confidence: 0.25 (street) + 0.15 (3 tokens) + 0.20 (city) = 0.60 ✅
"123 Main Street" + city="Brooklyn"
// Result: Passes 'smart' threshold, geocodes

// Confidence: 0.25 (street) + 0.00 (2 tokens) + 0.20 (city) = 0.45 ❌
"123 Main" + city="Brooklyn"
// Result: Below 'smart' threshold, skips

// Confidence: 0.25 + 0.15 + 0.10 (paste) = 0.50 ❌
"123 Main St, Brooklyn, NY" (pasted, but no fields filled yet)
// Result: Below 'smart' threshold, skips

// Confidence: 0.25 + 0.15 + 0.20 + 0.20 = 0.80 ✅
"123 Main Street" + city="Brooklyn" + state="New York"
// Result: Passes 'aggressive' threshold, geocodes

Impact:

  • basic → 0% reduction (baseline)
  • smart → 40-55% API call reduction ⭐ Best balance
  • aggressive → 60-80% reduction (may require too much data)

Balanced (Best for Most Use Cases)

env
ADDRESS_BLUR_REQUIRE_COMPLETENESS=basic  # City OR postal required
ADDRESS_BLUR_SMART_DETECTION=smart       # 0.60 confidence threshold

Result: ~50-65% API call reduction with excellent UX

Cost-Optimized (High Volume / Budget-Conscious)

env
ADDRESS_BLUR_REQUIRE_COMPLETENESS=complete  # City AND state required
ADDRESS_BLUR_SMART_DETECTION=aggressive     # 0.80 confidence threshold

Result: ~70-85% API call reduction, but users must fill more fields

Development (No Restrictions)

env
ADDRESS_BLUR_REQUIRE_COMPLETENESS=any    # No requirements
ADDRESS_BLUR_SMART_DETECTION=basic       # No filtering

Result: 0% reduction, fastest development experience

How the Guards Work Together

Blur geocoding uses a sequential guard system. Each guard can skip geocoding:

  1. Skip if being populated from reverse geocoding (prevents infinite loop)
  2. Skip if input is empty
  3. Skip if blur geocoding is disabled (ADDRESS_BLUR_GEOCODING_ENABLED=false)
  4. Skip if input too short (ADDRESS_BLUR_MIN_LENGTH)
  5. Skip if already verified (ADDRESS_BLUR_SKIP_IF_VERIFIED=true)
  6. Detect full-address entry (optional boost for confidence scoring)
  7. Check field completeness (ADDRESS_BLUR_REQUIRE_COMPLETENESS)
  8. Check confidence score (ADDRESS_BLUR_SMART_DETECTION)

If any guard returns "skip", geocoding doesn't run (saves API call).

Monitoring & Optimization

Check your configuration:

bash
# View current blur geocoding config
php artisan tinker
>>> config('addresses.blur_geocoding')

Monitor API usage:

  • View cache statistics: Admin Panel → Address API Cache
  • Check audit logs: Admin Panel → Address API Audit Logs
  • Track cost savings: Review cache hit ratio

Optimization tips:

  1. Start with basic + smart (recommended defaults)
  2. Monitor API costs for 1-2 weeks
  3. If costs too high → Try complete + smart or basic + aggressive
  4. If users frustrated → Relax settings (any + basic or basic + smart)
  5. Use cache statistics to measure impact of changes

International Addresses

The package supports 256 countries with intelligent address component extraction.

Why Structured Subdivision Data Matters

Most address systems store subdivisions as free text - users type "NY", "New York", "new york", or "N.Y." and each variant is stored as-is. Reports fragment, deduplication breaks, and you end up maintaining cleanup scripts forever.

Filament Address Pro takes a different approach. Every address links to a subdivision record by ULID, not by text string. The text field (administrative_area, locality, dependent_locality) is a display label; the ID (administrative_area_id, locality_id, dependent_locality_id) is the source of truth — a stable, canonical identifier that never drifts. Rename "Bombay" to "Mumbai" in the seed data and every address referencing that ID updates its display label automatically; the relationship was never stored as text. This has compounding benefits:

  • Unambiguous reporting - GROUP BY administrative_area_id returns exactly 50 US states - not 50 states plus "NY", "N.Y.", and "Calif." as separate buckets. BI queries work without fuzzy deduplication, and dashboards show clean geographic breakdowns from day one.

  • Fuzzy matching corrects errors - Import a spreadsheet with "Masachusetts" or "TX" and the package resolves it to the correct subdivision record via fuzzy matching. The typo or abbreviation is absorbed on entry - it never reaches your database. Other systems store whatever the user typed, then fight the ambiguity in every downstream query.

  • Local names alongside English - When ADDRESS_SHOW_LOCAL_NAMES=true, dropdown options display both the English name and the local-language name (e.g., "Tokyo / 東京都"). A Japanese admin searching for "東京都" and an English-speaking admin searching for "Tokyo" find the same record. This is practical for international teams - it eliminates the "which name do I search for?" problem.

  • Hierarchical queries without external services - The parent_id chain (district → city → state → country) lives in your database. "All addresses in Bavaria" is a single query through the hierarchy - no API call, no third-party geocoding, no string matching against "Bayern" vs "Bavaria" vs "BY".

  • Postal code to subdivision resolution - For countries where the system inherently supports this (FR, DE, ES, IT, NL), entering a postal code auto-populates the subdivision. Type "75007" and the form fills in "Paris" (département). This is only possible because the reference data includes postal prefix mappings.

  • Zero external dependencies - The 17,000+ subdivisions ship as JSON inside the package. No runtime API calls to populate dropdowns, no data service subscription, no risk of a third party deprecating an endpoint. Your data update path is composer update.

Country-Specific Formats

Different countries have different address formats. The package handles this automatically:

United States

1600 Pennsylvania Avenue NW
Washington, DC 20500

United Kingdom

10 Downing Street
Westminster
London SW1A 2AA

Japan (Hierarchical)

4-chōme-2-8 Shibakōen
Minato City
Tokyo 105-0011

South Korea

123 Gangnam-daero
Gangnam-gu
Seoul 06236

Working with Subdivisions

Countries have different subdivision levels (state/province → city → district):

php
use Viewflex\FilamentAddress\Models\Country;
use Viewflex\FilamentAddress\Services\CountryService;

// Get a country with hierarchical subdivisions
$china = Country::where('code', 'CN')->first();

// Check subdivision depth
$depth = $china->getActualSubdivisionDepth(); // 3 for China (province, city, district)

// Get subdivision options for dropdowns (hierarchical)
$provinces = CountryService::getAdministrativeAreaOptions('CN');
// Returns: ['province_id_1' => 'Beijing', 'province_id_2' => 'Shanghai', ...]

$cities = CountryService::getLocalityOptions('CN', $provinceId);
// Returns cities for the given province
// Example: ['city_id_1' => 'Dongcheng District', ...]

$districts = CountryService::getDependentLocalityOptions('CN', $cityId);
// Returns districts for the given city
// Example: ['district_id_1' => 'Wangfujing Subdistrict', ...]

// For countries with fewer levels (e.g., US has only states)
$us = Country::where('code', 'US')->first();
$depth = $us->getActualSubdivisionDepth(); // 1 for US (states only)

$states = CountryService::getAdministrativeAreaOptions('US');
// Returns: ['state_id_1' => 'Alabama', 'state_id_2' => 'Alaska', ...]

Formatting International Addresses

php
use Viewflex\FilamentAddress\Services\CountryService;

// International format (with country)
$formatted = CountryService::formatAddress('JP', [
    'address_line_1'    => '4-chōme-2-8 Shibakōen',
    'locality'          => 'Minato City',
    'administrative_area' => 'Tokyo',
    'postal_code'       => '105-0011',
]);
// Output:
// 4-chōme-2-8 Shibakōen
// Minato City, Tokyo 105-0011
// JAPAN

// Domestic format (no country, uses abbreviations)
$formatted = CountryService::formatAddressLocal('US', [
    'address_line_1'    => '1600 Pennsylvania Ave NW',
    'locality'          => 'Washington',
    'administrative_area' => 'District of Columbia',
    'postal_code'       => '20500',
]);
// Output:
// 1600 Pennsylvania Ave NW
// Washington, DC 20500

Postal Code Validation

php
use Viewflex\FilamentAddress\Models\Country;
use Viewflex\FilamentAddress\Services\CountryService;

// Get the postal code regex for a country, then validate
$pattern = Country::where('iso2', 'US')->first()->addressDefinition->postal_code_regex;

$isValid = CountryService::validatePostalCode($pattern, '20500');   // true
$isValid = CountryService::validatePostalCode($pattern, 'invalid'); // false

Address Verification

Verification ensures deliverability and improves data quality. In the default interactive mode, the user clicks Verify to trigger the side-by-side comparison dialog; the provider runs at that point, not automatically. In silent mode, verification runs automatically on save with no dialog — suited for public-facing forms where surfacing a comparison dialog is undesirable.

Verification Providers

The package supports multiple verification providers:

USPS (Free for US)

  • Best for: US addresses
  • Cost: Free
  • Accuracy: Authoritative US Postal Service data
  • Setup: Register at developer.usps.com
env
USPS_CONSUMER_KEY=your_consumer_key
USPS_CONSUMER_SECRET=your_consumer_secret

Google Address Validation

  • Best for: International addresses in supported countries
  • Cost: $0.005 per request
  • Coverage: ~40 countries
  • Setup: Enable in Google Cloud Console

Uses your existing Google Maps API key.

Smarty

  • Best for: High-volume US addresses (US Street API) or 240+ countries (International Street API)
  • Cost: ~$0.006 per lookup; plan-based pricing
  • Free tier: Available (trial)

Configuration

php
// config/addresses.php
'verification' => [
    'enabled' => true,
    'provider' => 'auto', // auto, usps, google, smarty, none

    // Auto-select provider by country
    'country_providers' => [
        'US' => 'usps',       // Free USPS for US
        // Others use Smarty or Google depending on provider_priority
    ],

    'fallback_to_geocoding' => true, // Use geocoding if verification fails
    'cache_ttl' => 86400, // Cache verification results for 24 hours
],

How Verification Works

The flow differs by input mode:

Autocomplete mode — Selecting a place from the Google Places search box is treated as implicit verification. The form sets is_verified = true with verification_provider = google_places immediately. No Verify button press is needed.

Geocoding and Manual modes (interactive) — The user fills in fields (blur geocoding auto-fills components on tab-out in geocoding mode), then clicks Verify. The configured provider runs and, if successful, checks whether the standardized version differs meaningfully from what was entered. If it does, a side-by-side dialog lets the user accept the standardized address or keep their original. If there are no significant differences, the verification is applied silently.

Silent mode — Verification runs automatically on save, with no dialog. The provider result is applied or discarded silently.

Verification Status Badge

A badge below the address fields reflects the current state. The green ✓ Verified by {Provider} and blue ✓ Accepted by user badges are clickable — clicking opens a small popover showing the provider name and verification date. A Re-verify Address button inside the popover re-triggers verification, useful when an address needs to be re-checked after a data correction.

Provider outcome:

  • If successful: Standardized components are applied, is_verified = true, provider name and metadata stored
  • If fails: Falls back to geocoded data (if available), is_verified = false, address still saved with best available data

Checking Verification Status

php
// Check if address is verified
if ($address->is_verified) {
    echo "Verified by: " . $address->verification_provider;
    echo "Verified at: " . $address->verified_at;

    // Access verification metadata
    $metadata = $address->verification_metadata;
    // Example USPS metadata:
    // ['delivery_point' => 'confirmed', 'dpv_match' => 'Y']
}

// Query verified addresses
use Viewflex\FilamentAddress\Models\Address;

$verified = Address::verified()->get();
$unverified = Address::unverified()->get();
$uspsVerified = Address::verifiedBy('usps')->get();

Manual Verification

You can programmatically verify addresses:

php
use Viewflex\FilamentAddress\Services\Verification\AddressVerificationService;

$verifier = app(AddressVerificationService::class);

$result = $verifier->verify([
    'address_line_1' => '1600 Pennsylvania Ave NW',
    'locality' => 'Washington',
    'administrative_area' => 'DC',
    'postal_code' => '20500',
    'country_code' => 'US',
]);

if ($result->isSuccessful()) {
    $provider = $result->getProvider(); // 'usps'
    $components = $result->getComponents();

    // Access verified components
    $address1 = $result->getAddressLine1();
    $city = $result->getCity();
    $state = $result->getState();
    $zip = $result->getPostalCode();
    $metadata = $result->getMetadata();
} else {
    $error = $result->getError();
}

Customization

Custom Address Form

Extend the base address form to add custom fields:

php
namespace App\Filament\Schemas;

use Viewflex\FilamentAddress\Filament\Schemas\AddressForm as BaseAddressForm;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Select;

class CustomAddressForm extends BaseAddressForm
{
    public static function schema(): array
    {
        return [
            ...parent::schema(),

            Select::make('address_type')
                ->label('Address Type')
                ->options([
                    'residential' => 'Residential',
                    'commercial' => 'Commercial',
                    'po_box' => 'PO Box',
                ])
                ->default('residential')
                ->required(),

            Textarea::make('delivery_notes')
                ->label('Delivery Instructions')
                ->maxLength(500)
                ->rows(3)
                ->hint('Special instructions for delivery'),

            TextInput::make('gate_code')
                ->label('Gate/Access Code')
                ->maxLength(20),
        ];
    }
}

Then use your custom form:

php
use App\Filament\Schemas\CustomAddressForm;

public static function form(Form $form): Form
{
    return $form->schema([
        ...CustomAddressForm::schema(),
    ]);
}

Custom Verification Provider

Implement your own verification provider:

php
namespace App\Services;

use Viewflex\FilamentAddress\Services\Verification\VerificationProvider;
use Viewflex\FilamentAddress\Services\Verification\VerificationResult;
use Illuminate\Support\Facades\Http;

class CanadaPostVerifier implements VerificationProvider
{
    public function verify(array $address): VerificationResult
    {
        // Make API request to Canada Post
        $response = Http::post('https://addresscomplete.canadapost.ca/verify', [
            'street' => $address['address_line_1'],
            'city' => $address['locality'],
            'province' => $address['administrative_area'],
            'postal_code' => $address['postal_code'],
        ]);

        if ($response->successful()) {
            $data = $response->json();

            return VerificationResult::success(
                provider: 'canada-post',
                components: [
                    'address_line_1' => $data['street'],
                    'locality' => $data['city'],
                    'administrative_area' => $data['province'],
                    'postal_code' => $data['postal_code'],
                    'country_code' => 'CA',
                ],
                metadata: [
                    'delivery_mode' => $data['delivery_mode'],
                    'precision' => $data['precision'],
                ]
            );
        }

        return VerificationResult::failure(
            provider: 'canada-post',
            error: 'Canada Post verification failed: ' . $response->body()
        );
    }

    public function supportsCountry(string $countryCode): bool
    {
        return $countryCode === 'CA';
    }

    public function getName(): string
    {
        return 'canada-post';
    }

    public function isConfigured(): bool
    {
        return !empty(config('services.canada_post.api_key'));
    }
}

Register in a service provider:

php
use App\Services\CanadaPostVerifier;
use Viewflex\FilamentAddress\Services\Verification\AddressVerificationService;

public function boot(): void
{
    $verificationService = app(AddressVerificationService::class);
    $verificationService->registerProvider(app(CanadaPostVerifier::class));
}

Configure in .env:

env
# Use Canada Post for CA addresses
ADDRESS_VERIFICATION_PROVIDER=auto

# Add to config/addresses.php
'verification' => [
    'country_providers' => [
        'CA' => 'canada-post',
    ],
],

Best Practices

1. Always Enable Verification

Address verification improves data quality and reduces delivery issues:

env
ADDRESS_VERIFICATION_ENABLED=true
ADDRESS_VERIFICATION_PROVIDER=auto

2. Use Primary Address Pattern

For models with multiple addresses, always designate one as primary:

php
// Good: Use the helper method
$customer->setPrimaryAddress($newAddress);

// Avoid: Direct updates (doesn't clear other primary flags)
$address->update(['is_primary' => true]);

3. Cache Geocoding Results

The package caches verification results by default. For custom geocoding:

php
use Illuminate\Support\Facades\Cache;

$cacheKey = 'geocode:' . md5($address);

$result = Cache::remember($cacheKey, 86400, function () use ($address, $country) {
    return app(GeocodingService::class)->geocode($address, $country);
});

4. Handle Verification Gracefully

Never block the user if verification fails:

php
// Good: Save with geocoded data if verification fails
$result = $geocoder->geocode($userInput, $country);
if ($result['success']) {
    $address->fill($result['components'])->save();
    // is_verified will be set appropriately
}

// Avoid: Throwing errors on verification failure
if (!$result['components']['is_verified']) {
    throw new Exception('Address verification failed'); // Bad!
}

5. Use Subdivisions When Available

Store subdivision IDs for better data integrity:

php
// Good: Store subdivision ID
$address->administrative_area_id = $stateId;
$address->administrative_area = 'New York'; // Fallback text

// Benefit: Can query by subdivision relationships
$addressesInNY = Address::where('administrative_area_id', $stateId)->get();

6. Validate Before Geocoding

Reduce unnecessary API calls:

php
// Validate required fields before geocoding
if (empty($data['address_line_1']) || empty($data['country_code'])) {
    return; // Don't geocode incomplete addresses
}

// Check if address changed before re-geocoding
if ($address->address_line_1 === $oldAddress) {
    return; // Don't re-geocode unchanged address
}

7. Monitor API Usage

Track your Google Maps API usage to avoid surprise bills:

  • Set up billing alerts in Google Cloud Console
  • Use caching aggressively
  • Consider limiting autocomplete suggestions
  • Use USPS (free) for US addresses when possible

Troubleshooting

Geocoding Returns No Results

Symptoms: $result['success'] = false, error: "No geocoding results found"

Solutions:

  1. Check address format - Ensure address includes street, city, and country
  2. Verify API key - Check GOOGLE_MAPS_SERVER_KEY is set and has the Geocoding API enabled
  3. Enable Geocoding API - Confirm it's enabled in Google Cloud Console
  4. Check API restrictions - Ensure your domain/IP is allowed
  5. Test with known address - Try "1600 Pennsylvania Ave NW, Washington DC, US"
php
// Debug geocoding
$result = $geocoder->geocode('1600 Pennsylvania Ave NW', 'US');
if (!$result['success']) {
    logger()->error('Geocoding failed', [
        'error' => $result['error'],
        'address' => '1600 Pennsylvania Ave NW',
        'country' => 'US',
    ]);
}

Verification Always Fails

Symptoms: is_verified = false for all addresses

Solutions:

  1. Check provider is configured - Verify USPS keys or Google API
  2. Confirm provider supports country - USPS only works for US
  3. Check verification is enabled - ADDRESS_VERIFICATION_ENABLED=true
  4. Review logs - Look for logger()->info() messages about verification
  5. Test provider directly - Use verification service standalone
php
// Test USPS provider
$provider = app(UspsProvider::class);
if (!$provider->isConfigured()) {
    logger()->error('USPS not configured');
}

$result = $provider->verify([
    'address_line_1' => '1600 Pennsylvania Ave NW',
    'locality' => 'Washington',
    'administrative_area' => 'DC',
    'postal_code' => '20500',
    'country_code' => 'US',
]);

logger()->info('USPS result', [
    'success' => $result->isSuccessful(),
    'error' => $result->getError(),
]);

Subdivisions Not Loading

Symptoms: State/city dropdowns are empty

Solutions:

  1. Seed country data:

    bash
    php artisan db:seed --class=Viewflex\\FilamentAddress\\Database\\Seeders\\CountriesDomainSeeder
  2. Verify subdivisions exist:

    php
    use Viewflex\FilamentAddress\Models\Country;
    
    $us = Country::where('code', 'US')->first();
    $states = $us->subdivisions()->count();
    logger()->info("US has {$states} states");
  3. Check subdivision depth:

    php
    $depth = $us->getActualSubdivisionDepth();
    // US should return 1 (states)

Autocomplete Not Appearing

Note: Google Places Autocomplete is available in the address search field (type in the search box after selecting a country). If it isn't appearing, verify your Google Maps API key is set and ADDRESS_INPUT_MODE is auto or autocomplete, and that the Maps JavaScript API and Places API are enabled in Google Cloud Console.

Address Preview Not Updating

Symptoms: Live preview doesn't refresh when fields change

Solutions:

  1. Clear cache - Run php artisan config:clear
  2. Check browser console - Look for JavaScript errors

Next Steps


Questions or issues? Contact support@viewflex.net

Built with ❤️ by Viewflex

Released under a commercial license.