PHPackages                             christianrey/laravel-addressable - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. christianrey/laravel-addressable

ActiveLibrary[Database &amp; ORM](/categories/database)

christianrey/laravel-addressable
================================

Polymorphic postal addresses for Eloquent models, with purpose roles and an optional cost-aware Google enrichment layer.

v0.1.0(2mo ago)05MITPHPPHP ^8.2CI passing

Since May 25Pushed 2mo agoCompare

[ Source](https://github.com/christianpasinrey/laravel-addressable)[ Packagist](https://packagist.org/packages/christianrey/laravel-addressable)[ RSS](/packages/christianrey-laravel-addressable/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependencies (9)Versions (2)Used By (0)

laravel-addressable
===================

[](#laravel-addressable)

Polymorphic postal addresses for Eloquent models — purpose roles (main / shipping / tax), a hybrid international schema, **local-first geographic resolution** from open data, and an **optional, cost-aware** Google enrichment layer.

- 🏠 **Addresses for any model** — a polymorphic `Address` + an `Addressable` trait, with main / shipping / tax roles enforced per owner.
- 🗺️ **Local-first geo** — resolve country, region/province and postal code from your own tables (open [GeoNames](https://www.geonames.org/) data). No API calls, no per-lookup cost.
- 💸 **Optional Google enrichment** — autocomplete, geocoding and address validation, opt-in per tier, with cost shown before any billable call and the API key kept server-side.
- 🧰 **Headless** — backend + artisan CLI. Bring your own frontend.

**Requires:** PHP 8.2+ · Laravel 11, 12 or 13.

Install
-------

[](#install)

```
composer require christianrey/laravel-addressable
php artisan vendor:publish --tag="addressable-migrations"
php artisan migrate
```

Publish the config if you want to tweak table names, tiers, pricing or routes:

```
php artisan vendor:publish --tag="addressable-config"
```

Addresses &amp; roles
---------------------

[](#addresses--roles)

Add the trait to any model:

```
use ChristianRey\Addressable\Concerns\Addressable;

class Customer extends Model
{
    use Addressable;
}
```

```
use ChristianRey\Addressable\Data\AddressData;

$customer->addAddress(
    new AddressData(
        line1: 'C/ Boiro 1',
        locality: 'Boiro',
        adminArea: 'A Coruña',
        postalCode: '15930',
        countryCode: 'ES',
    ),
    roles: ['main', 'shipping'],
);

$customer->mainAddress();      // ?Address
$customer->shippingAddress();  // ?Address
$customer->taxAddress();       // ?Address
$customer->setMainAddress($address);
```

Each owner holds **at most one** `main`, one `shipping` and one `tax` address; a single address may carry several roles at once. Assigning a role atomically demotes the previous holder. The schema is hybrid: typed, queryable columns (`country_code`, `postal_code`, `locality`, `admin_area`, `latitude`, `longitude`, …) plus a `components` JSON blob and a `formatted_address`.

Local-first geographic data
---------------------------

[](#local-first-geographic-data)

Resolve **country, region/province and postal code** from package-owned tables — **no API calls, no per-lookup cost**. Data comes from open [GeoNames](https://www.geonames.org/) dumps (CC-BY 4.0), imported on demand.

### It is not Spain-only

[](#it-is-not-spain-only)

The engine works for **any country GeoNames covers** — you import the ones you need by ISO 3166-1 code:

```
php artisan addressable:import-geo --country=ES --country=PT --country=FR --country=DE
```

Each `--country=XX` downloads and imports that country's regions and postal codes into your local tables (idempotent — safe to re-run). Nothing is bundled in the package; a fresh install starts with empty geo tables until you run this.

### Resolving

[](#resolving)

```
use ChristianRey\Addressable\Geo\GeoResolver;

$resolver = app(GeoResolver::class);

$resolver->country('ES');             // Country (name, currency_code, calling_code)
$resolver->subdivisions('ES');        // Collection of top-level regions
$pc = $resolver->postalCode('ES', '15930');

$pc->place_name;    // "Boiro"
$pc->admin1_name;   // "Galicia"   (top-level region)
$pc->admin2_name;   // "A Coruña"  (province / second-level region)
$pc->latitude;      // 42.6483
$pc->longitude;     // -8.8857
```

### Coverage &amp; caveats (honest)

[](#coverage--caveats-honest)

- **Countries &amp; regions** exist for virtually every country. **Postal codes** are available for the ~100 countries GeoNames provides them for (most of Europe, North America, and many others) — coverage and granularity **vary by country**.
- Region names are stored generically as `admin1_name` (top-level) and `admin2_name` (second-level). **Which level means "province" depends on the country.** In Spain, `admin1_name` is the autonomous community ("Galicia") and `admin2_name` is the province ("A Coruña"); elsewhere the meaningful level differs. The package stores both; you interpret them for your locale.
- Postal → region linking uses the names from the postal data directly (not the GeoNames admin *codes*, which don't reliably match across countries), so the region/province is correct even where code-based joins fail.

Geographic data is provided by GeoNames under the [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/) license.

Optional Google enrichment
--------------------------

[](#optional-google-enrichment)

Disabled by default (the `null` driver refuses enrichment calls). Enable the driver and only the tiers you want:

```
ADDRESSABLE_GEOCODER=google
ADDRESSABLE_GOOGLE_KEY=your-key
```

```
// config/addressable.php
'geocoding' => [
    'google' => [
        'tiers' => [
            'autocomplete' => true,
            'geocoding' => true,
            'address_validation' => false,
        ],
    ],
],
```

Enable the matching Google Cloud APIs: **Places API (New)** (autocomplete), **Geocoding API** (geocode), **Address Validation API** (validate).

```
use ChristianRey\Addressable\Geocoding\Contracts\Geocoder;

$geocoder = app(Geocoder::class);

$geocoder->autocomplete('Gran Vía, Madrid', sessionToken: $token); // list
$geocoder->geocode('Puerta del Sol, Madrid');                      // ?AddressData
$geocoder->validate($addressData);                                 // AddressValidationResult
```

A disabled tier (or a missing key) throws a typed exception — `TierDisabledException`, `MissingApiKeyException`, `QuotaExceededException` or `MalformedResponseException` — so failures are never silent. Set quota caps in Google Cloud Console to keep testing within the free tier.

CLI
---

[](#cli)

```
# Interactively choose tiers; prints the config + .env to apply (never rewrites files).
php artisan addressable:configure

# Fire a real request through the configured driver. Billable tiers require --confirm
# and the per-call cost is shown first.
php artisan addressable:test "Puerta del Sol, Madrid" --type=geocode --confirm
php artisan addressable:test "Calle de Alcalá 1, 28014 Madrid" --type=validate --confirm

# Compare Google's published prices against the stored reference values (best-effort,
# falls back to the official URL if the page can't be read).
php artisan addressable:check-google-pricing
```

Reference prices live in `config/addressable.php` under `geocoding.pricing` — editable, with an `updated_at` per SKU. They are a reference, not a live quote; always confirm against Google's pricing page.

Server-side proxy routes (optional)
-----------------------------------

[](#server-side-proxy-routes-optional)

Disabled by default. When enabled, your frontend calls these endpoints and the Google API key **stays on the server** — it is never sent to the browser.

```
ADDRESSABLE_ROUTES=true
```

```
// config/addressable.php
'routes' => [
    'enabled' => env('ADDRESSABLE_ROUTES', false),
    'prefix' => 'addressable',
    'middleware' => ['throttle:60,1'], // add 'auth' etc. as needed
],
```

Method &amp; pathBodyReturns`POST /addressable/autocomplete``{ "input": "…", "sessionToken"? }``{ "suggestions": [{ "placeId", "text" }] }``POST /addressable/geocode``{ "address": "…" }``{ "result": { …AddressData } | null }``POST /addressable/validate``{ "line1": "…", "line2"?, "country_code"? }``{ "complete": bool, "address": { …AddressData } }`A disabled tier returns `409`; quota `429`; an unconfigured key `503`; any upstream error `502` — all with generic bodies. Google's raw response is never forwarded to the client.

Testing
-------

[](#testing)

```
composer test        # or: vendor/bin/pest
```

The Google driver is faked in tests — the suite never makes real network calls.

License
-------

[](#license)

MIT. See [LICENSE](LICENSE). Geographic data © GeoNames, CC-BY 4.0.

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance88

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity36

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

61d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/7e3fdf42ac2e1111ed8a0ae93b9c92131d42f571f5a8910fcb40a9bc879294f0?d=identicon)[christianpasinrey](/maintainers/christianpasinrey)

---

Top Contributors

[![christianpasinrey](https://avatars.githubusercontent.com/u/70205361?v=4)](https://github.com/christianpasinrey "christianpasinrey (53 commits)")

---

Tags

addressesgeocodinglaravellaravelgeocodingaddressaddressablepolymorphic

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/christianrey-laravel-addressable/health.svg)

```
[![Health](https://phpackages.com/badges/christianrey-laravel-addressable/health.svg)](https://phpackages.com/packages/christianrey-laravel-addressable)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[spatie/laravel-health

Monitor the health of a Laravel application

87912.0M177](/packages/spatie-laravel-health)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M246](/packages/laravel-ai)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5022.6k](/packages/simplestats-io-laravel-client)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[masterix21/laravel-licensing

Laravel licensing package with polymorphic assignment to any model, activation keys, expirations/renewals, and seat control via LicenseUsage. Supports offline verification with public-key–signed tokens, a CLI to generate/rotate/revoke keys, and an extensible architecture via config and contracts.

1613.3k4](/packages/masterix21-laravel-licensing)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
