Commit Graph

1 Commits

Author SHA1 Message Date
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