2026-04-14 08:20:42 +00:00
|
|
|
[](https://github.com/blax-software)
|
|
|
|
|
|
|
|
|
|
# Laravel Addresses
|
|
|
|
|
|
|
|
|
|
[](https://php.net)
|
|
|
|
|
[](https://laravel.com)
|
|
|
|
|
|
|
|
|
|
Universal Laravel address management — from rural GPS coordinates to specific rooms inside skyscrapers, worldwide.
|
|
|
|
|
|
|
|
|
|
## Overview
|
|
|
|
|
|
|
|
|
|
This package provides a complete address management system for Laravel applications built on a **three-layer architecture**:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Address → The physical place (street, city, coordinates …)
|
|
|
|
|
└── AddressLink → Connects an address to a model with a purpose (User's "Office")
|
|
|
|
|
└── AddressAssignment → References a link from another context (Job's "pickup")
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Example:** A user has an office address. A job references that office as its pickup location — without duplicating the address data.
|
|
|
|
|
|
|
|
|
|
## Features
|
|
|
|
|
|
|
|
|
|
- **15 address fields** — street-level to room-level precision, with GPS coordinates (WGS-84) and altitude
|
|
|
|
|
- **Polymorphic links** — attach addresses to any Eloquent model
|
|
|
|
|
- **17 built-in link types** — Home, Office, Shipping, Billing, Warehouse and more
|
|
|
|
|
- **Address assignments** — reference someone else's address in another context
|
|
|
|
|
- **Temporal validity** — `active_from` / `active_until` on every link
|
|
|
|
|
- **AddressService** — distance calculations (Haversine), proximity queries, duplicate detection, coordinate conversion
|
feat: opt-in Nominatim geocoding observer + sync UUID migrations
Adds an AddressObserver that resolves latitude/longitude from the
postal fields after every save, serializing calls cluster-wide through
a Cache::lock and respecting Nominatim's 1-req/sec usage policy.
How the rate limit holds across workers
---------------------------------------
* Cache::lock('addresses:geocoding:lock') ensures only ONE process
ever talks upstream at a time (any LockProvider-capable store works:
redis / memcached / database / file / array).
* Inside the lock the driver reads a shared :last_call_at timestamp
and usleep()s the difference to the configured min_interval before
hitting the API, then stamps the new value right after the response
comes back — pacing the next caller from when we *actually stopped*
talking upstream, not from when the lock was first taken.
* Lock TTL (15s default) bounds crash-recovery time so a kill -9
can't pin the lock open forever.
Observer behaviour
------------------
* Hooks 'created' (always geocodes on insert) and 'updated' (only when
a *postal* field actually changed — wasChanged() on street, city,
postal_code, country_code, …). Lat/lon edits don't loop because
the write-back uses saveQuietly().
* Errors (LockTimeoutException, network, 5xx) are logged and
swallowed — the user's save() / create() call is never made fatal
by a transient upstream issue.
* Honours an update_only_when_missing toggle so apps can preserve
manually-entered coordinates.
Backward compatibility
----------------------
* Default 'enabled' is FALSE — upgrading the package does NOT start
making outbound HTTP calls on existing apps. Apps that want the
feature opt in via ADDRESSES_GEOCODING_ENABLED=true and a
descriptive ADDRESSES_GEOCODING_USER_AGENT (OSMF blocks generic UAs).
* The Geocoder contract is bound through the container so apps can
rebind to Google Maps / Mapbox / a self-hosted Nominatim without
touching the observer.
Pre-existing fix shipped alongside
----------------------------------
The Address* models switched to HasUuids in 73c14c5, but neither the
published migration stub nor the workbench fixture migration were
updated — every test in the suite was failing with 'datatype mismatch'
on the auto-increment integer PK. Both migrations now use uuid('id')
+ foreignUuid() so the model trait and the schema agree.
For consumers who already published the stub: no change. Your
existing migration file is untouched. Only fresh `vendor:publish
--tag=addresses-migrations` runs pick up the UUID shape.
New files
---------
src/Services/Geocoding/Contracts/Geocoder.php
src/Services/Geocoding/GeocodingResult.php
src/Services/Geocoding/NominatimGeocoder.php
src/Observers/AddressObserver.php
tests/Unit/GeocodingTest.php
docs/geocoding.md
Tests
-----
221 / 221 (100%) Time: ~2.8s
The 14 new tests cover URL/param construction, User-Agent, empty- and
unparseable-result handling, the rate-limit timing assertion (proves
two back-to-back calls take >= min_interval), cache-lock
serialization, observer pre-fill on create, no-loop on lat/lon
update, re-geocode on postal change, the update_only_when_missing
policy, the enabled master switch, and graceful swallowing of both
LockTimeoutException and generic upstream errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 08:03:55 +00:00
|
|
|
- **Auto-geocoding** — saves call Nominatim (OpenStreetMap) for lat/lon, serialized by a Cache lock and paced at 1 req/sec
|
2026-04-14 08:20:42 +00:00
|
|
|
- **Fully configurable** — custom model classes, table names, default link type
|
|
|
|
|
- **Soft deletes** on addresses, cascade deletes on links and assignments
|
|
|
|
|
|
|
|
|
|
## Requirements
|
|
|
|
|
|
|
|
|
|
- PHP 8.1+
|
|
|
|
|
- Laravel 9, 10, 11 or 12
|
|
|
|
|
- `blax-software/laravel-workkit` (installed automatically)
|
|
|
|
|
|
|
|
|
|
## Installation
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
composer require blax-software/laravel-addresses
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Publish and run the migrations:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
php artisan vendor:publish --tag="addresses-migrations"
|
|
|
|
|
php artisan migrate
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Optionally publish the config:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
php artisan vendor:publish --tag="addresses-config"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Quick Start
|
|
|
|
|
|
|
|
|
|
### 1. Add the trait to your model
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
use Blax\Addresses\Traits\HasAddresses;
|
|
|
|
|
|
|
|
|
|
class User extends Model
|
|
|
|
|
{
|
|
|
|
|
use HasAddresses;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. Create and attach an address
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
use Blax\Addresses\Enums\AddressLinkType;
|
|
|
|
|
|
|
|
|
|
$link = $user->addAddress([
|
|
|
|
|
'street' => '350 Fifth Avenue',
|
|
|
|
|
'city' => 'New York',
|
|
|
|
|
'state' => 'NY',
|
|
|
|
|
'postal_code' => '10118',
|
|
|
|
|
'country_code' => 'US',
|
|
|
|
|
'latitude' => 40.748817,
|
|
|
|
|
'longitude' => -73.985428,
|
|
|
|
|
], AddressLinkType::Office);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. Query addresses
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
$user->addresses; // all addresses
|
|
|
|
|
$user->addressesOfType(AddressLinkType::Office); // only offices
|
|
|
|
|
$user->primaryAddress(); // primary across all types
|
|
|
|
|
$user->activeAddressLinks(); // only currently active links
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 4. Assign an address to another model
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
use Blax\Addresses\Traits\HasAddressAssignments;
|
|
|
|
|
|
|
|
|
|
class Job extends Model
|
|
|
|
|
{
|
|
|
|
|
use HasAddressAssignments;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$job->assignAddressLink($link, 'pickup');
|
|
|
|
|
$job->assignedAddressForRole('pickup'); // → the Address model
|
|
|
|
|
```
|
|
|
|
|
|
feat: opt-in Nominatim geocoding observer + sync UUID migrations
Adds an AddressObserver that resolves latitude/longitude from the
postal fields after every save, serializing calls cluster-wide through
a Cache::lock and respecting Nominatim's 1-req/sec usage policy.
How the rate limit holds across workers
---------------------------------------
* Cache::lock('addresses:geocoding:lock') ensures only ONE process
ever talks upstream at a time (any LockProvider-capable store works:
redis / memcached / database / file / array).
* Inside the lock the driver reads a shared :last_call_at timestamp
and usleep()s the difference to the configured min_interval before
hitting the API, then stamps the new value right after the response
comes back — pacing the next caller from when we *actually stopped*
talking upstream, not from when the lock was first taken.
* Lock TTL (15s default) bounds crash-recovery time so a kill -9
can't pin the lock open forever.
Observer behaviour
------------------
* Hooks 'created' (always geocodes on insert) and 'updated' (only when
a *postal* field actually changed — wasChanged() on street, city,
postal_code, country_code, …). Lat/lon edits don't loop because
the write-back uses saveQuietly().
* Errors (LockTimeoutException, network, 5xx) are logged and
swallowed — the user's save() / create() call is never made fatal
by a transient upstream issue.
* Honours an update_only_when_missing toggle so apps can preserve
manually-entered coordinates.
Backward compatibility
----------------------
* Default 'enabled' is FALSE — upgrading the package does NOT start
making outbound HTTP calls on existing apps. Apps that want the
feature opt in via ADDRESSES_GEOCODING_ENABLED=true and a
descriptive ADDRESSES_GEOCODING_USER_AGENT (OSMF blocks generic UAs).
* The Geocoder contract is bound through the container so apps can
rebind to Google Maps / Mapbox / a self-hosted Nominatim without
touching the observer.
Pre-existing fix shipped alongside
----------------------------------
The Address* models switched to HasUuids in 73c14c5, but neither the
published migration stub nor the workbench fixture migration were
updated — every test in the suite was failing with 'datatype mismatch'
on the auto-increment integer PK. Both migrations now use uuid('id')
+ foreignUuid() so the model trait and the schema agree.
For consumers who already published the stub: no change. Your
existing migration file is untouched. Only fresh `vendor:publish
--tag=addresses-migrations` runs pick up the UUID shape.
New files
---------
src/Services/Geocoding/Contracts/Geocoder.php
src/Services/Geocoding/GeocodingResult.php
src/Services/Geocoding/NominatimGeocoder.php
src/Observers/AddressObserver.php
tests/Unit/GeocodingTest.php
docs/geocoding.md
Tests
-----
221 / 221 (100%) Time: ~2.8s
The 14 new tests cover URL/param construction, User-Agent, empty- and
unparseable-result handling, the rate-limit timing assertion (proves
two back-to-back calls take >= min_interval), cache-lock
serialization, observer pre-fill on create, no-loop on lat/lon
update, re-geocode on postal change, the update_only_when_missing
policy, the enabled master switch, and graceful swallowing of both
LockTimeoutException and generic upstream errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 08:03:55 +00:00
|
|
|
### 5. Automatic geocoding (opt-in)
|
|
|
|
|
|
|
|
|
|
Set `ADDRESSES_GEOCODING_ENABLED=true` to have an observer ask Nominatim
|
|
|
|
|
(OpenStreetMap) for `latitude` / `longitude` after every save. Calls are
|
|
|
|
|
serialized cluster-wide through a `Cache::lock` and paced at 1 req/sec
|
|
|
|
|
to honour the OSMF usage policy. **Off by default** so an upgrade
|
|
|
|
|
doesn't surprise existing apps with new outbound HTTP traffic.
|
|
|
|
|
|
|
|
|
|
```ini
|
|
|
|
|
# .env
|
|
|
|
|
ADDRESSES_GEOCODING_ENABLED=true
|
|
|
|
|
# OSMF policy: identify your app — generic UAs get blocked.
|
|
|
|
|
ADDRESSES_GEOCODING_USER_AGENT="my-app (https://example.com)"
|
|
|
|
|
ADDRESSES_GEOCODING_EMAIL="ops@example.com"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
$address = Address::create([
|
|
|
|
|
'street' => 'Stephansplatz 1',
|
|
|
|
|
'postal_code' => '1010',
|
|
|
|
|
'city' => 'Vienna',
|
|
|
|
|
'country_code' => 'AT',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Observer has filled these in by the time create() returns.
|
|
|
|
|
$address->refresh();
|
|
|
|
|
$address->latitude; // 48.2082…
|
|
|
|
|
$address->longitude; // 16.3738…
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Tunable knobs (see [`config/addresses.php`](config/addresses.php) →
|
|
|
|
|
`geocoding`):
|
|
|
|
|
|
|
|
|
|
- `enabled` — master switch (env: `ADDRESSES_GEOCODING_ENABLED`)
|
|
|
|
|
- `driver` — only `nominatim` ships out of the box, but you can rebind
|
|
|
|
|
the `Geocoder` contract to plug in Google Maps / Mapbox / Mapquest /
|
|
|
|
|
a self-hosted Nominatim
|
|
|
|
|
- `update_only_when_missing` — leave manually-entered coordinates alone
|
|
|
|
|
- `min_interval_seconds` — global ceiling on outbound traffic (default
|
|
|
|
|
`1.0`, matches Nominatim's published policy)
|
|
|
|
|
- `lock_wait_seconds`, `lock_ttl_seconds` — Cache lock parameters
|
|
|
|
|
- `accept_language` — Nominatim's `display_name` localization
|
|
|
|
|
- `drivers.nominatim.user_agent` / `email` — required by Nominatim for
|
|
|
|
|
attribution; set both in production
|
|
|
|
|
|
|
|
|
|
For tests / imports, disable per-environment with
|
|
|
|
|
`ADDRESSES_GEOCODING_ENABLED=false` (or
|
|
|
|
|
`config()->set('addresses.geocoding.enabled', false)` inside a TestCase
|
|
|
|
|
hook).
|
|
|
|
|
|
|
|
|
|
### 6. Use the AddressService
|
2026-04-14 08:20:42 +00:00
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
// Via helper
|
|
|
|
|
$distance = address()->distanceBetween($addressA, $addressB); // km
|
|
|
|
|
|
|
|
|
|
// Nearby addresses within 10 km
|
|
|
|
|
$nearby = address()->nearby(48.2082, 16.3738, 10);
|
|
|
|
|
|
|
|
|
|
// Format for display
|
|
|
|
|
echo address()->formatMultiline($address);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Documentation
|
|
|
|
|
|
|
|
|
|
| Guide | Description |
|
|
|
|
|
|----------------------------------------------------------------|------------------------------------------------------|
|
|
|
|
|
| [Installation & Configuration](docs/installation.md) | Setup, publishing, config options |
|
|
|
|
|
| [Core Concepts](docs/core-concepts.md) | The three-layer architecture explained |
|
|
|
|
|
| [HasAddresses Trait](docs/has-addresses.md) | Full API for address-owning models |
|
|
|
|
|
| [HasAddressAssignments Trait](docs/has-address-assignments.md) | Full API for address-consuming models |
|
|
|
|
|
| [AddressService](docs/address-service.md) | Distance, proximity, formatting, conversion |
|
feat: opt-in Nominatim geocoding observer + sync UUID migrations
Adds an AddressObserver that resolves latitude/longitude from the
postal fields after every save, serializing calls cluster-wide through
a Cache::lock and respecting Nominatim's 1-req/sec usage policy.
How the rate limit holds across workers
---------------------------------------
* Cache::lock('addresses:geocoding:lock') ensures only ONE process
ever talks upstream at a time (any LockProvider-capable store works:
redis / memcached / database / file / array).
* Inside the lock the driver reads a shared :last_call_at timestamp
and usleep()s the difference to the configured min_interval before
hitting the API, then stamps the new value right after the response
comes back — pacing the next caller from when we *actually stopped*
talking upstream, not from when the lock was first taken.
* Lock TTL (15s default) bounds crash-recovery time so a kill -9
can't pin the lock open forever.
Observer behaviour
------------------
* Hooks 'created' (always geocodes on insert) and 'updated' (only when
a *postal* field actually changed — wasChanged() on street, city,
postal_code, country_code, …). Lat/lon edits don't loop because
the write-back uses saveQuietly().
* Errors (LockTimeoutException, network, 5xx) are logged and
swallowed — the user's save() / create() call is never made fatal
by a transient upstream issue.
* Honours an update_only_when_missing toggle so apps can preserve
manually-entered coordinates.
Backward compatibility
----------------------
* Default 'enabled' is FALSE — upgrading the package does NOT start
making outbound HTTP calls on existing apps. Apps that want the
feature opt in via ADDRESSES_GEOCODING_ENABLED=true and a
descriptive ADDRESSES_GEOCODING_USER_AGENT (OSMF blocks generic UAs).
* The Geocoder contract is bound through the container so apps can
rebind to Google Maps / Mapbox / a self-hosted Nominatim without
touching the observer.
Pre-existing fix shipped alongside
----------------------------------
The Address* models switched to HasUuids in 73c14c5, but neither the
published migration stub nor the workbench fixture migration were
updated — every test in the suite was failing with 'datatype mismatch'
on the auto-increment integer PK. Both migrations now use uuid('id')
+ foreignUuid() so the model trait and the schema agree.
For consumers who already published the stub: no change. Your
existing migration file is untouched. Only fresh `vendor:publish
--tag=addresses-migrations` runs pick up the UUID shape.
New files
---------
src/Services/Geocoding/Contracts/Geocoder.php
src/Services/Geocoding/GeocodingResult.php
src/Services/Geocoding/NominatimGeocoder.php
src/Observers/AddressObserver.php
tests/Unit/GeocodingTest.php
docs/geocoding.md
Tests
-----
221 / 221 (100%) Time: ~2.8s
The 14 new tests cover URL/param construction, User-Agent, empty- and
unparseable-result handling, the rate-limit timing assertion (proves
two back-to-back calls take >= min_interval), cache-lock
serialization, observer pre-fill on create, no-loop on lat/lon
update, re-geocode on postal change, the update_only_when_missing
policy, the enabled master switch, and graceful swallowing of both
LockTimeoutException and generic upstream errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 08:03:55 +00:00
|
|
|
| [Geocoding](docs/geocoding.md) | Auto lat/lon via Nominatim, cache lock, rate limit |
|
2026-04-14 08:20:42 +00:00
|
|
|
| [AddressLinkType Enum](docs/address-link-types.md) | All 17 built-in types with descriptions |
|
|
|
|
|
| [Customization](docs/customization.md) | Extending models, custom tables, overriding defaults |
|
|
|
|
|
|
|
|
|
|
## Testing
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
composer test
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## License
|
|
|
|
|
|
|
|
|
|
MIT
|
2026-05-05 14:44:32 +00:00
|
|
|
|
|
|
|
|
## Star History
|
|
|
|
|
|
|
|
|
|
<a href="https://www.star-history.com/?repos=blax-software%2Flaravel-addresses&type=date&legend=top-left">
|
|
|
|
|
<picture>
|
|
|
|
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=blax-software/laravel-addresses&type=date&theme=dark&legend=top-left" />
|
|
|
|
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=blax-software/laravel-addresses&type=date&legend=top-left" />
|
|
|
|
|
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=blax-software/laravel-addresses&type=date&legend=top-left" />
|
|
|
|
|
</picture>
|
|
|
|
|
</a>
|