PHPackages                             masterix21/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. masterix21/laravel-addressable

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

masterix21/laravel-addressable
==============================

Addresses for any Eloquent model

2.4.0(1mo ago)734.3k↑492.9%4MITPHPPHP ^8.2CI passing

Since Aug 4Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/masterix21/laravel-addressable)[ Packagist](https://packagist.org/packages/masterix21/laravel-addressable)[ Docs](https://github.com/masterix21/laravel-addressable)[ Fund](https://github.com/masterix21)[ GitHub Sponsors](https://github.com/masterix21)[ RSS](/packages/masterix21-laravel-addressable/feed)WikiDiscussions master Synced 2d ago

READMEChangelog (10)Dependencies (39)Versions (23)Used By (0)

Make your Eloquent models addressable
=====================================

[](#make-your-eloquent-models-addressable)

[![Latest Version on Packagist](https://camo.githubusercontent.com/56a8ce2b4fac88bcb1574948bc2436730408740e65828006050f0eda6fdcd39a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d6173746572697832312f6c61726176656c2d6164647265737361626c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/masterix21/laravel-addressable)[![GitHub Tests Action Status](https://camo.githubusercontent.com/70ba825558067fde5b30878cabdcc9b6e2f6176a24472650b34ce56c285fe197/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6d6173746572697832312f6c61726176656c2d6164647265737361626c652f72756e2d74657374732e796d6c3f6272616e63683d6d6173746572266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/masterix21/laravel-addressable/actions?query=workflow%3Arun-tests+branch%3Amaster)[![Total Downloads](https://camo.githubusercontent.com/a41cc3e0c45be9b439bb7dd6b091ce0f6912293f0d99c1fba0ce90dc44cdd4a6/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6d6173746572697832312f6c61726176656c2d6164647265737361626c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/masterix21/laravel-addressable)

Attach any number of addresses to any Eloquent model through a polymorphic relation. Built on top of [`matanyadaev/laravel-eloquent-spatial`](https://github.com/MatanYadaev/laravel-eloquent-spatial), it natively supports geospatial coordinates and distance queries — perfect for billing, shipping or any location-aware use case.

Features
--------

[](#features)

- Polymorphic `addresses()` relation for any Eloquent model
- Dedicated `billing` and `shipping` traits with primary-address shortcuts
- Eager-loadable `primaryAddress`, `billingAddress`, `shippingAddress` relations
- Primary-address toggling, scoped per address type, with events
- Geospatial `POINT` column with distance queries, `withinRadius` scope and optional spatial index
- Free-form `meta` JSON column for extra data
- Configurable `display_address` accessor
- SoftDeletes-aware cascade delete of addresses when the parent model is deleted
- Pluggable `Address` model and table name

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11.x, 12.x or 13.x (Laravel 13 requires PHP 8.3+)
- A database with spatial support (MySQL 8+, MariaDB 10.5+, PostgreSQL with PostGIS)

Support us
----------

[](#support-us)

If you like my work, you can [sponsor me](https://github.com/sponsors/masterix21).

Installation
------------

[](#installation)

Install the package via Composer:

```
composer require masterix21/laravel-addressable
```

Publish and run the migrations:

```
php artisan vendor:publish --provider="Masterix21\Addressable\AddressableServiceProvider" --tag="migrations"
php artisan migrate
```

Optionally publish the config file:

```
php artisan vendor:publish --provider="Masterix21\Addressable\AddressableServiceProvider" --tag="config"
```

### Upgrading from 2.1.x

[](#upgrading-from-21x)

Publish and run the additional `meta` column migration:

```
php artisan vendor:publish --provider="Masterix21\Addressable\AddressableServiceProvider" --tag="addressable-meta-migration"
php artisan migrate
```

### Upgrading from 2.2.x

[](#upgrading-from-22x)

**Spatial index (optional but recommended).** Publish and run the spatial index migration to make `coordinates` indexed for fast distance queries:

```
php artisan vendor:publish --provider="Masterix21\Addressable\AddressableServiceProvider" --tag="addressable-spatial-index-migration"
php artisan migrate
```

The migration:

- Backfills any row with `NULL` coordinates to `POINT(0, 0)` with the configured SRID.
- Alters `coordinates` to `NOT NULL` with a `POINT(0, 0)` default, so addresses created without explicit coordinates keep working.
- Adds a `SPATIAL INDEX` on `coordinates`.

**`primaryAddress` is now a relation.** It used to be a method returning `?Address`. It now returns a `MorphOne` relation, which means:

- `$user->primaryAddress` still returns `?Address` (unchanged via property access).
- `$user->primaryAddress()` now returns a relation builder, **not** the model. If you were calling it as a method, switch to the property or append `->first()`.
- You can now eager load it: `User::with('primaryAddress')->get()`.

Configuration
-------------

[](#configuration)

The published `config/addressable.php` file exposes:

```
return [
    'models' => [
        // Swap with your own model (e.g. to use UUIDs).
        'address' => \Masterix21\Addressable\Models\Address::class,
    ],

    'tables' => [
        // Change before running the migration.
        'addresses' => 'addresses',
    ],

    // SRID used for the POINT column. 4326 = WGS84 (lat/lng).
    'srid' => 4326,

    // Template for the display_address accessor. Use {field_name} placeholders.
    // Set to null to fall back to the default " - " separated format.
    'display_format' => null,
];
```

Usage
-----

[](#usage)

### Attach addresses to a model

[](#attach-addresses-to-a-model)

```
use Masterix21\Addressable\Models\Concerns\HasAddresses;

class User extends Model
{
    use HasAddresses;
}

$user->addresses; // MorphMany of Masterix21\Addressable\Models\Address
```

`HasAddresses` is the generic trait. For billing or shipping flows, use the dedicated traits (they can be combined):

```
use Masterix21\Addressable\Models\Concerns\HasBillingAddresses;
use Masterix21\Addressable\Models\Concerns\HasShippingAddresses;

class User extends Model
{
    use HasBillingAddresses, HasShippingAddresses;
}

$user->billingAddress;    // Primary billing address (MorphOne)
$user->billingAddresses;  // All billing addresses (MorphMany)

$user->shippingAddress;   // Primary shipping address (MorphOne)
$user->shippingAddresses; // All shipping addresses (MorphMany)
```

When the parent model is hard-deleted, its addresses are automatically removed. If the parent uses `SoftDeletes`, addresses survive soft-delete and are removed only on `forceDelete()`.

### Create addresses

[](#create-addresses)

```
// Generic address
$user->addAddress([
    'label' => 'Home',
    'street_address1' => 'Via Roma 1',
    'zip' => '20100',
    'city' => 'Milan',
    'state' => 'MI',
    'country' => 'IT',
]);

// Billing address — is_billing is set automatically
$user->addBillingAddress([
    'street_address1' => 'Via Roma 1',
    'city' => 'Milan',
]);

// Shipping address — is_shipping is set automatically
$user->addShippingAddress([
    'street_address1' => 'Via Roma 1',
    'city' => 'Milan',
]);

// Fetch the primary address (any type) via the eager-loadable relation
$user->primaryAddress;                  // ?Address
User::with('primaryAddress')->get();    // eager loaded
```

### Address fields

[](#address-fields)

FieldTypeNotes`label`stringOptional tag (e.g. "Home", "Office")`is_primary`boolToggled via `markPrimary()``is_billing`boolSet automatically by the helper`is_shipping`boolSet automatically by the helper`street_address1`string`street_address2`string`zip`string`city`string`state`string`country`stringISO alpha-2/3 (max 4 chars)`coordinates``Point`Cast to a spatial Point object`meta`arrayJSON column for arbitrary data### Mark an address as primary

[](#mark-an-address-as-primary)

`markPrimary()` ensures a single primary address per type, scoped to the same parent model. It is wrapped in a transaction and unmarks any other primary address of the same kind.

```
$shippingAddress->markPrimary();
$shippingAddress->unmarkPrimary();

$billingAddress->markPrimary();
$billingAddress->unmarkPrimary();
```

### Events

[](#events)

Every primary toggle dispatches dedicated events (each carrying the `Address` instance):

ActionGeneric eventBilling eventShipping event`markPrimary()``AddressPrimaryMarked``BillingAddressPrimaryMarked``ShippingAddressPrimaryMarked``unmarkPrimary()``AddressPrimaryUnmarked``BillingAddressPrimaryUnmarked``ShippingAddressPrimaryUnmarked`All events live in `Masterix21\Addressable\Events`. Billing/shipping variants fire only when the respective flag is set on the address.

### Query scopes

[](#query-scopes)

```
use Masterix21\Addressable\Models\Address;

Address::query()->primary()->get();
Address::query()->billing()->get();
Address::query()->shipping()->get();

// Scopes are composable
Address::query()->billing()->primary()->first();
```

### Inverse relationship

[](#inverse-relationship)

```
$address->addressable; // The parent model (User, Company, ...)
```

### Metadata

[](#metadata)

Every address has a JSON `meta` column for extra data without touching the schema:

```
$user->addAddress([
    'street_address1' => 'Via Roma 1',
    'city' => 'Milan',
    'meta' => [
        'phone' => '+39 02 1234567',
        'floor' => 3,
        'notes' => 'Ring twice',
    ],
]);

$address->meta['phone']; // '+39 02 1234567'
```

### Display address

[](#display-address)

The `display_address` accessor returns a readable representation:

```
$address->display_address; // "Via Roma 1 - 20100 - Milan - MI - IT"
```

Customize the format in `config/addressable.php`:

```
'display_format' => '{street_address1}, {street_address2}, {zip} {city}, {state}, {country}',
```

Geospatial features
-------------------

[](#geospatial-features)

### Store coordinates

[](#store-coordinates)

```
use MatanYadaev\EloquentSpatial\Objects\Point;

$user->addBillingAddress([
    'street_address1' => 'Via Antonio Izzi de Falenta, 7/C',
    'zip' => '88100',
    'city' => 'Catanzaro',
    'state' => 'CZ',
    'country' => 'IT',
    'coordinates' => new Point(38.90852, 16.5894, config('addressable.srid')),
]);

// Or assign later
$billingAddress->coordinates = new Point(38.90852, 16.5894, config('addressable.srid'));
$billingAddress->save();
```

### Filter by distance

[](#filter-by-distance)

Use the `withinRadius` scope for the common case of "addresses within N meters of a point":

```
use MatanYadaev\EloquentSpatial\Objects\Point;

$milano = new Point(45.4391, 9.1906, config('addressable.srid'));

// Addresses within 10 km of Milan
Address::query()->withinRadius($milano, 10_000)->get();
```

For custom comparisons (`=`, etc.) drop down to the underlying spatial scope:

```
Address::query()
    ->whereDistanceSphere(
        column: 'coordinates',
        geometryOrColumn: $milano,
        operator: '>=',
        value: 10_000,
    )
    ->get();
```

### Add distance as a column

[](#add-distance-as-a-column)

`addDistanceTo()` appends the distance from a given point (always in **meters**) as an extra column. Divide by `1000` for kilometers, by `1609.344` for miles.

```
$origin = new Point(45.4642, 9.1900, config('addressable.srid'));

// Default column name: `distance`
$addresses = Address::query()
    ->addDistanceTo($origin)
    ->get();

$addresses->first()->distance; // e.g. 1523.4

// Custom column name
Address::query()->addDistanceTo($origin, as: 'dist_meters')->get();

// Nearest first
Address::query()->addDistanceTo($origin)->orderBy('distance')->get();
```

### Order by distance and find nearest

[](#order-by-distance-and-find-nearest)

`orderByDistance()` sorts addresses by distance from a point without adding any column. `nearest()` is the high-level helper for "give me the N closest addresses": it adds the `distance` column, orders ascending and optionally applies a limit.

```
$milano = new Point(45.4642, 9.1900, config('addressable.srid'));

// The 5 addresses closest to Milan, each with a populated `distance` (meters)
$closest = Address::query()->nearest($milano, 5)->get();

$closest->first()->distance; // e.g. 42.1

// Composable with any other scope
Address::query()->billing()->nearest($milano, 3)->get();

// Without a limit, ordering is applied but the result set is not truncated
Address::query()->shipping()->nearest($milano)->paginate(20);

// Ordering only, no `distance` column
Address::query()->orderByDistance($milano, 'desc')->get();
```

Geocoding
---------

[](#geocoding)

Geocoding turns a textual address into coordinates, and reverse geocoding does the opposite. Drivers are tried in order (FIFO): the first one returning a result wins, so a failing driver falls back to the next.

Two keyless drivers ship enabled by default: **Nominatim** (OpenStreetMap) and **Photon** (Komoot).

### Google driver

[](#google-driver)

A **Google** driver is provided as well. It is commented out in `config/addressable.php` — uncomment its entry and set `GOOGLE_GEOCODER_KEY`:

```
// config/addressable.php
'geocoding' => [
    'drivers' => [
        'google' => [
            'class' => \Masterix21\Addressable\Geocoding\Drivers\GoogleGeocoder::class,
            'endpoint' => 'https://maps.googleapis.com/maps/api/geocode/json',
            'api_key' => env('GOOGLE_GEOCODER_KEY'),
        ],
        'nominatim' => [/* ... */],
        'photon' => [/* ... */],
    ],
],
```

Drivers are tried in array order, so placing `google` first makes it the primary driver with Nominatim and Photon as fallbacks. The Google driver uses the Geocoding API for both forward and reverse geocoding, and treats any response whose `status` is not `OK` as a miss (falling through to the next driver).

### Geocode an address

[](#geocode-an-address)

```
$address = $user->addAddress([
    'street_address1' => 'Via Roma 1',
    'zip' => '20100',
    'city' => 'Milan',
    'country' => 'IT',
]);

if ($address->geocode()) {  // fills `coordinates`, does not persist
    $address->save();
}
```

### Reverse geocode coordinates

[](#reverse-geocode-coordinates)

```
$address->coordinates = new Point(45.4642, 9.19, config('addressable.srid'));

if ($address->reverseGeocode()) {  // fills street/zip/city/state/country
    $address->save();
}
```

Both methods emit the `AddressGeocoded` event on success.

### Automatic geocoding

[](#automatic-geocoding)

Enable `addressable.geocoding.auto` (or `ADDRESSABLE_GEOCODER_AUTO=true`) to geocode addresses saved without coordinates automatically:

```
// With auto enabled, coordinates are resolved on save
$user->addAddress([
    'street_address1' => 'Via Roma 1',
    'city' => 'Milan',
    'country' => 'IT',
]);
```

When enabled, the `addressable.geocoding.job` is dispatched for every address saved without coordinates. The default `GeocodeAddressJob` job runs **synchronously**. To geocode on a queue instead — recommended for web requests, so the geocoder HTTP call doesn't block the response — extend the job and point the config at your subclass:

```
use Illuminate\Contracts\Queue\ShouldQueue;
use Masterix21\Addressable\Jobs\GeocodeAddressJob;

class QueuedGeocodeAddressJob extends GeocodeAddressJob implements ShouldQueue
{
}
```

```
// config/addressable.php
'geocoding' => [
    'job' => \App\Jobs\QueuedGeocodeAddressJob::class,
],
```

> **Note:** Nominatim and Photon are free public services with a usage policy (Nominatim limits to ~1 request/second). For high volume, use Google or a self-hosted instance, and throttle bulk geocoding on your side.

### Custom drivers

[](#custom-drivers)

Implement `Masterix21\Addressable\Geocoding\Contracts\Geocoder` and add it to `addressable.geocoding.drivers`. Each driver receives its own config block (plus the shared `srid` and `user_agent`).

AI guidelines (Laravel Boost)
-----------------------------

[](#ai-guidelines-laravel-boost)

This package ships [Laravel Boost](https://github.com/laravel/boost) guidelines. If your app uses Boost, run `php artisan boost:install` (or `php artisan boost:update --discover`) and select `laravel-addressable` to give your AI agent package-specific instructions.

Testing
-------

[](#testing)

```
composer test
```

Run the suite in parallel with `composer test-parallel`.

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security
--------

[](#security)

If you discover any security related issues, please email  instead of using the issue tracker.

Credits
-------

[](#credits)

- [Luca Longo](https://github.com/masterix21)
- [Matan Yadaev](https://github.com/MatanYadaev/laravel-eloquent-spatial)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

59

—

FairBetter than 98% of packages

Maintenance91

Actively maintained with recent releases

Popularity38

Limited adoption so far

Community13

Small or concentrated contributor base

Maturity77

Established project with proven stability

 Bus Factor1

Top contributor holds 97.9% 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

Every ~111 days

Recently: every ~16 days

Total

20

Last Release

50d ago

Major Versions

1.3.0 → 2.0.02024-09-23

PHP version history (6 changes)1.0.0PHP ^7.4

1.0.4PHP ^7.3|^8.0

1.1.0PHP ^8.1

1.3.0PHP ^8.1|^8.2|^8.3

2.0.1PHP ^8.2|^8.3|^8.4

2.2.0PHP ^8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/177020fc4adb5c08acee3e6fe0b65002a96665c5d5c522a3ef009b4105fd634f?d=identicon)[masterix](/maintainers/masterix)

---

Top Contributors

[![masterix21](https://avatars.githubusercontent.com/u/6555012?v=4)](https://github.com/masterix21 "masterix21 (95 commits)")[![bdj-Razik](https://avatars.githubusercontent.com/u/82386537?v=4)](https://github.com/bdj-Razik "bdj-Razik (1 commits)")[![phpMagpie](https://avatars.githubusercontent.com/u/1027681?v=4)](https://github.com/phpMagpie "phpMagpie (1 commits)")

---

Tags

laravel-addressablemasterix21

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

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

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

###  Alternatives

[illuminate/database

The Illuminate Database package.

2.8k54.9M11.6k](/packages/illuminate-database)[bavix/laravel-wallet

It's easy to work with a virtual wallet.

1.3k1.3M19](/packages/bavix-laravel-wallet)[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.8M47](/packages/spatie-laravel-pdf)[psalm/plugin-laravel

Psalm plugin for Laravel

3355.3M346](/packages/psalm-plugin-laravel)[wnx/laravel-backup-restore

A package to restore database backups made with spatie/laravel-backup.

213421.0k2](/packages/wnx-laravel-backup-restore)[lacodix/laravel-model-filter

A Laravel package to filter, search and sort models with ease while fetching from database.

17558.6k](/packages/lacodix-laravel-model-filter)

PHPackages © 2026

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