Go to file
Fabian @ Blax Software 5a5bab040a 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 10:03:55 +02:00
config feat: opt-in Nominatim geocoding observer + sync UUID migrations 2026-05-12 10:03:55 +02:00
database feat: opt-in Nominatim geocoding observer + sync UUID migrations 2026-05-12 10:03:55 +02:00
docs feat: opt-in Nominatim geocoding observer + sync UUID migrations 2026-05-12 10:03:55 +02:00
src feat: opt-in Nominatim geocoding observer + sync UUID migrations 2026-05-12 10:03:55 +02:00
tests/Unit feat: opt-in Nominatim geocoding observer + sync UUID migrations 2026-05-12 10:03:55 +02:00
.gitattributes Initial release 2026-04-14 10:20:42 +02:00
.gitignore Initial release 2026-04-14 10:20:42 +02:00
LICENSE Initial release 2026-04-14 10:20:42 +02:00
README.md feat: opt-in Nominatim geocoding observer + sync UUID migrations 2026-05-12 10:03:55 +02:00
composer.json feat: switch address models to UUIDs and accept string IDs in trait helpers 2026-04-17 11:03:02 +02:00
phpunit.xml Initial release 2026-04-14 10:20:42 +02:00
pint.json Initial release 2026-04-14 10:20:42 +02:00

README.md

Blax Software OSS

Laravel Addresses

PHP Version Laravel

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 validityactive_from / active_until on every link
  • AddressService — distance calculations (Haversine), proximity queries, duplicate detection, coordinate conversion
  • Auto-geocoding — saves call Nominatim (OpenStreetMap) for lat/lon, serialized by a Cache lock and paced at 1 req/sec
  • 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

composer require blax-software/laravel-addresses

Publish and run the migrations:

php artisan vendor:publish --tag="addresses-migrations"
php artisan migrate

Optionally publish the config:

php artisan vendor:publish --tag="addresses-config"

Quick Start

1. Add the trait to your model

use Blax\Addresses\Traits\HasAddresses;

class User extends Model
{
    use HasAddresses;
}

2. Create and attach an address

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

$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

use Blax\Addresses\Traits\HasAddressAssignments;

class Job extends Model
{
    use HasAddressAssignments;
}

$job->assignAddressLink($link, 'pickup');
$job->assignedAddressForRole('pickup'); // → the Address model

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.

# .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"
$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.phpgeocoding):

  • 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

// 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 Setup, publishing, config options
Core Concepts The three-layer architecture explained
HasAddresses Trait Full API for address-owning models
HasAddressAssignments Trait Full API for address-consuming models
AddressService Distance, proximity, formatting, conversion
Geocoding Auto lat/lon via Nominatim, cache lock, rate limit
AddressLinkType Enum All 17 built-in types with descriptions
Customization Extending models, custom tables, overriding defaults

Testing

composer test

License

MIT

Star History

Star History Chart