2026-04-14 08:20:42 +00:00
|
|
|
<?php
|
|
|
|
|
|
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
|
|
|
use Blax\Addresses\Enums\AddressLinkType;
|
|
|
|
|
use Blax\Addresses\Models\Address;
|
|
|
|
|
use Blax\Addresses\Models\AddressAssignment;
|
|
|
|
|
use Blax\Addresses\Models\AddressLink;
|
|
|
|
|
|
2026-04-14 08:20:42 +00:00
|
|
|
return [
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Model Classes
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
| Override these with your own model classes if you need to extend or
|
|
|
|
|
| customise the package models. Your custom models should extend the
|
|
|
|
|
| corresponding package model so that migrations and relationships
|
|
|
|
|
| continue to work out of the box.
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
'models' => [
|
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
|
|
|
'address' => Address::class,
|
|
|
|
|
'address_link' => AddressLink::class,
|
|
|
|
|
'address_assignment' => AddressAssignment::class,
|
2026-04-14 08:20:42 +00:00
|
|
|
],
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Table Names
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
| The database table names used by the package. Change these if they
|
|
|
|
|
| collide with existing tables in your application.
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
'table_names' => [
|
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
|
|
|
'addresses' => 'addresses',
|
|
|
|
|
'address_links' => 'address_links',
|
|
|
|
|
'address_assignments' => 'address_assignments',
|
2026-04-14 08:20:42 +00:00
|
|
|
],
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Default Address Link Type
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
| The default AddressLinkType applied when attaching an address to a model
|
|
|
|
|
| without specifying a type explicitly.
|
|
|
|
|
|
|
|
|
|
|
*/
|
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
|
|
|
'default_link_type' => AddressLinkType::Other,
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Geocoding
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
| After an Address is saved, the package can resolve its `latitude` and
|
|
|
|
|
| `longitude` from the postal fields using a public geocoder. By default
|
|
|
|
|
| this uses Nominatim (OpenStreetMap) — free, no API key, requires a
|
|
|
|
|
| descriptive User-Agent and a hard limit of one request per second
|
|
|
|
|
| (operations.osmfoundation.org/policies/nominatim/).
|
|
|
|
|
|
|
|
|
|
|
| The observer holds a Laravel `Cache::lock` while it makes the call, so
|
|
|
|
|
| even when the same code is running in multiple workers only ONE call
|
|
|
|
|
| hits the upstream at a time, and the 1-req/sec floor is enforced
|
|
|
|
|
| globally across the cluster via a shared cache timestamp.
|
|
|
|
|
|
|
|
|
|
|
| Set `enabled => false` to turn the observer off (e.g. in CI). Set
|
|
|
|
|
| `update_only_when_missing => true` to keep manually-entered coordinates
|
|
|
|
|
| and only geocode rows whose coordinates are still null.
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
'geocoding' => [
|
|
|
|
|
|
|
|
|
|
// Master switch. Opt-in by default — turning it on starts firing
|
|
|
|
|
// outbound HTTP calls on every Address save, which apps updating
|
|
|
|
|
// from a previous version don't want as a surprise. Flip it via
|
|
|
|
|
// ADDRESSES_GEOCODING_ENABLED=true once you've reviewed the
|
|
|
|
|
// Nominatim usage policy and set a proper User-Agent below.
|
|
|
|
|
'enabled' => env('ADDRESSES_GEOCODING_ENABLED', false),
|
|
|
|
|
|
|
|
|
|
// Driver — currently only `nominatim` is shipped. The Geocoder
|
|
|
|
|
// contract is in `src/Services/Geocoding/Contracts/Geocoder.php`
|
|
|
|
|
// so apps can bind their own implementation if they need Google
|
|
|
|
|
// Maps / Mapbox / etc.
|
|
|
|
|
'driver' => env('ADDRESSES_GEOCODING_DRIVER', 'nominatim'),
|
|
|
|
|
|
|
|
|
|
// True → only geocode when latitude/longitude are still NULL.
|
|
|
|
|
// • Manually-entered coordinates win, the observer leaves them alone.
|
|
|
|
|
// False → re-geocode every time a postal field actually changes.
|
|
|
|
|
// • Coordinates always track the textual address.
|
|
|
|
|
'update_only_when_missing' => env('ADDRESSES_GEOCODING_ONLY_WHEN_MISSING', false),
|
|
|
|
|
|
|
|
|
|
// Cache store used for the lock + the global "last call at" stamp.
|
|
|
|
|
// null = the app's default cache store. Pick something shared
|
|
|
|
|
// (redis / memcached / database) if you run multiple workers,
|
|
|
|
|
// otherwise the 1-req/sec ceiling is only per-process.
|
|
|
|
|
'cache_store' => env('ADDRESSES_GEOCODING_CACHE_STORE'),
|
|
|
|
|
|
|
|
|
|
// Cache key prefix — gives operators a single root to flush.
|
|
|
|
|
'cache_prefix' => 'addresses:geocoding',
|
|
|
|
|
|
|
|
|
|
// How long to wait (seconds) for the global geocoding lock before
|
|
|
|
|
// giving up. Bursts of saves queue up against this; pick a value
|
|
|
|
|
// that's roughly `expected_burst_size * min_interval`.
|
|
|
|
|
'lock_wait_seconds' => env('ADDRESSES_GEOCODING_LOCK_WAIT', 10),
|
|
|
|
|
|
|
|
|
|
// Lock TTL — protects against a hard crash leaving the lock held.
|
|
|
|
|
// Should comfortably exceed `timeout_seconds + min_interval_seconds`.
|
|
|
|
|
'lock_ttl_seconds' => env('ADDRESSES_GEOCODING_LOCK_TTL', 15),
|
|
|
|
|
|
|
|
|
|
// Minimum interval (seconds, float-friendly) between two consecutive
|
|
|
|
|
// upstream calls. Nominatim's published policy is "no more than 1
|
|
|
|
|
// per second". Don't go below 1.0 on the public server.
|
|
|
|
|
'min_interval_seconds' => env('ADDRESSES_GEOCODING_MIN_INTERVAL', 1.0),
|
|
|
|
|
|
|
|
|
|
// HTTP read+connect timeout for a single upstream call (seconds).
|
|
|
|
|
'timeout_seconds' => env('ADDRESSES_GEOCODING_TIMEOUT', 8),
|
|
|
|
|
|
|
|
|
|
// Languages preference (Accept-Language). Nominatim uses this to
|
|
|
|
|
// pick localized `display_name` strings.
|
|
|
|
|
'accept_language' => env('ADDRESSES_GEOCODING_LANG', 'en'),
|
|
|
|
|
|
|
|
|
|
// Driver-specific settings.
|
|
|
|
|
'drivers' => [
|
|
|
|
|
'nominatim' => [
|
|
|
|
|
// Base endpoint. Use a self-hosted instance here to lift
|
|
|
|
|
// the 1-req/sec restriction; see
|
|
|
|
|
// https://github.com/mediagis/nominatim-docker.
|
|
|
|
|
'endpoint' => env('ADDRESSES_GEOCODING_NOMINATIM_URL', 'https://nominatim.openstreetmap.org/search'),
|
|
|
|
|
|
|
|
|
|
// Nominatim's usage policy requires a descriptive User-Agent
|
|
|
|
|
// that identifies your application. The default uses the
|
|
|
|
|
// package name; SET YOUR OWN APP NAME + CONTACT in
|
|
|
|
|
// production so the OSMF can reach you if you're causing
|
|
|
|
|
// load problems instead of blocking your IP cold.
|
|
|
|
|
'user_agent' => env(
|
|
|
|
|
'ADDRESSES_GEOCODING_USER_AGENT',
|
|
|
|
|
'blax-software/laravel-addresses (https://github.com/blax-software/laravel-addresses)',
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Optional contact email — included as the `email` query
|
|
|
|
|
// parameter as suggested by the Nominatim docs.
|
|
|
|
|
'email' => env('ADDRESSES_GEOCODING_EMAIL'),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
],
|
2026-04-14 08:20:42 +00:00
|
|
|
|
|
|
|
|
];
|