2026-04-14 08:20:42 +00:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace Blax\Addresses\Migrations;
|
|
|
|
|
|
|
|
|
|
|
|
use Illuminate\Database\Migrations\Migration;
|
|
|
|
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
|
|
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
|
|
|
|
|
|
|
|
return new class extends Migration
|
|
|
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Run the migrations.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Creates two tables:
|
|
|
|
|
|
* - `addresses` – stores the physical address data (location, coordinates, etc.)
|
|
|
|
|
|
* - `address_links` – polymorphic pivot linking addresses to any Eloquent model
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function up(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
/*
|
|
|
|
|
|
|----------------------------------------------------------------------
|
|
|
|
|
|
| addresses — the canonical address record
|
|
|
|
|
|
|----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
| Designed to support worldwide formats, from a GPS point in the
|
|
|
|
|
|
| wilderness to a specific room inside a skyscraper.
|
|
|
|
|
|
|
|
|
|
|
|
|
| Coordinate system:
|
|
|
|
|
|
| latitude / longitude → WGS-84 decimal degrees
|
|
|
|
|
|
| altitude → metres Above Mean Sea Level (AMSL)
|
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
Schema::create(config('addresses.table_names.addresses', 'addresses'), function (Blueprint $table) {
|
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
|
|
|
|
// UUID PK — the Address model uses `HasUuids`. If you've already
|
|
|
|
|
|
// published a previous version of this stub with `$table->id()`
|
|
|
|
|
|
// (auto-increment), keep that — but you'll need to either drop
|
|
|
|
|
|
// `HasUuids` from your Address override or migrate the column to
|
|
|
|
|
|
// a UUID. Don't change a populated table's PK shape blindly.
|
|
|
|
|
|
$table->uuid('id')->primary();
|
2026-04-14 08:20:42 +00:00
|
|
|
|
|
|
|
|
|
|
// ── Street-level addressing ─────────────────────────────
|
|
|
|
|
|
// Primary street line (street name + house/building number).
|
|
|
|
|
|
$table->string('street')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// Additional line for c/o, suite, apartment, P.O. box, etc.
|
|
|
|
|
|
$table->string('street_extra')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// ── Building / indoor precision ─────────────────────────
|
|
|
|
|
|
// Building or complex name (e.g. "Empire State Building", "Block C").
|
|
|
|
|
|
$table->string('building')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// Floor / level inside the building (string to allow "GF", "B2", "Mezzanine").
|
|
|
|
|
|
$table->string('floor')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// Room, suite or unit number / name.
|
|
|
|
|
|
$table->string('room')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// ── Postal / administrative divisions ───────────────────
|
|
|
|
|
|
// Postal / ZIP code.
|
|
|
|
|
|
$table->string('postal_code')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// City, town, village or locality name.
|
|
|
|
|
|
$table->string('city')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// State, province, canton, prefecture or equivalent.
|
|
|
|
|
|
$table->string('state')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// County, district or other sub-state administrative area.
|
|
|
|
|
|
$table->string('county')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// ISO 3166-1 alpha-2 country code (e.g. "AT", "US", "JP").
|
|
|
|
|
|
$table->string('country_code', 2)->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// ── Coordinates (WGS-84) ────────────────────────────────
|
|
|
|
|
|
// Latitude in decimal degrees (−90 to +90).
|
|
|
|
|
|
$table->decimal('latitude', 10, 7)->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// Longitude in decimal degrees (−180 to +180).
|
|
|
|
|
|
$table->decimal('longitude', 10, 7)->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// Altitude in metres above mean sea level (AMSL). Positive = above, negative = below.
|
|
|
|
|
|
$table->decimal('altitude', 10, 2)->nullable();
|
|
|
|
|
|
|
2026-04-17 05:07:11 +00:00
|
|
|
|
// ── Building amenities ───────────────────────────────────
|
|
|
|
|
|
// Whether the building has an elevator (relevant for moving, deliveries).
|
|
|
|
|
|
$table->boolean('has_elevator')->default(false);
|
|
|
|
|
|
|
2026-04-14 08:20:42 +00:00
|
|
|
|
// ── Additional properties ───────────────────────────────
|
|
|
|
|
|
// Free-form notes (e.g. delivery instructions, landmark descriptions).
|
|
|
|
|
|
$table->text('notes')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// Flexible JSON bucket for any extra data the consuming app needs
|
|
|
|
|
|
// (e.g. Plus Codes, what3words, timezone, formatted display string).
|
|
|
|
|
|
$table->json('meta')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
$table->timestamps();
|
|
|
|
|
|
$table->softDeletes();
|
|
|
|
|
|
|
|
|
|
|
|
// ── Indexes ─────────────────────────────────────────────
|
|
|
|
|
|
$table->index('country_code');
|
|
|
|
|
|
$table->index('postal_code');
|
|
|
|
|
|
$table->index('city');
|
|
|
|
|
|
$table->index(['latitude', 'longitude']);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|----------------------------------------------------------------------
|
|
|
|
|
|
| address_links — polymorphic pivot ("addressable")
|
|
|
|
|
|
|----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
| Links an address to any Eloquent model (User, Company, Order …).
|
|
|
|
|
|
| The same address row can be linked to many models, and a single model
|
|
|
|
|
|
| can have many addresses (each with a different purpose / type).
|
|
|
|
|
|
|
|
|
|
|
|
|
| `type` – enum value describing the purpose of this link
|
|
|
|
|
|
| `label` – optional free-text override (handy for "Other" type)
|
|
|
|
|
|
| `is_primary` – marks ONE link per type per model as the primary
|
|
|
|
|
|
| `active_from` – when this link becomes effective
|
|
|
|
|
|
| `active_until`– when this link expires / is superseded
|
|
|
|
|
|
| `meta` – JSON bucket for developer-defined extra data
|
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
Schema::create(config('addresses.table_names.address_links', 'address_links'), function (Blueprint $table) {
|
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
|
|
|
|
$table->uuid('id')->primary();
|
2026-04-14 08:20:42 +00:00
|
|
|
|
|
|
|
|
|
|
// ── Foreign key to the address ──────────────────────────
|
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
|
|
|
|
$table->foreignUuid('address_id')
|
2026-04-14 08:20:42 +00:00
|
|
|
|
->constrained(config('addresses.table_names.addresses', 'addresses'))
|
|
|
|
|
|
->cascadeOnDelete();
|
|
|
|
|
|
|
|
|
|
|
|
// ── Polymorphic owner ───────────────────────────────────
|
|
|
|
|
|
// The model this address is linked to (e.g. App\Models\User, App\Models\Company).
|
|
|
|
|
|
$table->morphs('addressable');
|
|
|
|
|
|
|
|
|
|
|
|
// ── Link semantics ──────────────────────────────────────
|
|
|
|
|
|
// The purpose of this link, drawn from AddressLinkType enum.
|
|
|
|
|
|
$table->string('type')->default('other');
|
|
|
|
|
|
|
|
|
|
|
|
// Optional human-readable label to refine or override the type
|
|
|
|
|
|
// (useful when type = "other" or when several links share the same type).
|
|
|
|
|
|
$table->string('label')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// Whether this is the primary address for this type on the model.
|
|
|
|
|
|
$table->boolean('is_primary')->default(false);
|
|
|
|
|
|
|
|
|
|
|
|
// ── Temporal validity ───────────────────────────────────
|
|
|
|
|
|
// When this link becomes active (null = immediately).
|
|
|
|
|
|
$table->timestamp('active_from')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// When this link ceases to be active (null = indefinitely).
|
|
|
|
|
|
$table->timestamp('active_until')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// ── Extra data ──────────────────────────────────────────
|
|
|
|
|
|
// JSON bucket for any developer-defined data on the pivot
|
|
|
|
|
|
// (e.g. delivery window, access codes, department reference).
|
|
|
|
|
|
$table->json('meta')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
$table->timestamps();
|
|
|
|
|
|
|
|
|
|
|
|
// ── Indexes ─────────────────────────────────────────────
|
|
|
|
|
|
$table->index(['addressable_type', 'addressable_id', 'type'], 'addr_link_owner_type');
|
|
|
|
|
|
$table->index('type');
|
|
|
|
|
|
$table->index('is_primary');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|----------------------------------------------------------------------
|
|
|
|
|
|
| address_assignments — reference an AddressLink from another context
|
|
|
|
|
|
|----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
| While an AddressLink says "this Address belongs to User X as their
|
|
|
|
|
|
| Office", an AddressAssignment says "Job Y uses that specific link
|
|
|
|
|
|
| for its pickup location."
|
|
|
|
|
|
|
|
|
|
|
|
|
| `role` – context-specific purpose (e.g. "pickup", "delivery", "origin")
|
|
|
|
|
|
| `label` – optional free-text label
|
|
|
|
|
|
| `meta` – JSON bucket for developer-defined data
|
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
Schema::create(config('addresses.table_names.address_assignments', 'address_assignments'), function (Blueprint $table) {
|
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
|
|
|
|
$table->uuid('id')->primary();
|
2026-04-14 08:20:42 +00:00
|
|
|
|
|
|
|
|
|
|
// ── Foreign key to the address link ─────────────────────
|
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
|
|
|
|
$table->foreignUuid('address_link_id')
|
2026-04-14 08:20:42 +00:00
|
|
|
|
->constrained(config('addresses.table_names.address_links', 'address_links'))
|
|
|
|
|
|
->cascadeOnDelete();
|
|
|
|
|
|
|
|
|
|
|
|
// ── Polymorphic consumer ────────────────────────────────
|
|
|
|
|
|
// The model this address link is assigned to (e.g. Job, Order, Event).
|
|
|
|
|
|
$table->morphs('assignable');
|
|
|
|
|
|
|
|
|
|
|
|
// ── Assignment semantics ────────────────────────────────
|
|
|
|
|
|
// Context-specific role for this assignment (e.g. "pickup", "delivery").
|
|
|
|
|
|
$table->string('role')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// Optional human-readable label.
|
|
|
|
|
|
$table->string('label')->nullable();
|
|
|
|
|
|
|
2026-04-17 05:07:11 +00:00
|
|
|
|
// ── Contact / access overrides ──────────────────────────
|
|
|
|
|
|
// These let the assignment specify WHO to contact and WHERE
|
|
|
|
|
|
// exactly within the building, independent of the base address.
|
|
|
|
|
|
// The address stays generic ("Main Street 5, 1010 Vienna")
|
|
|
|
|
|
// while the assignment adds context ("3rd floor, door 12, ring Müller").
|
|
|
|
|
|
|
|
|
|
|
|
// Name on the door / bell / intercom.
|
|
|
|
|
|
$table->string('name_on_door')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// Contact email for this specific assignment.
|
|
|
|
|
|
$table->string('email')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// Contact phone for this specific assignment.
|
|
|
|
|
|
$table->string('phone')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// Floor override (when the base address doesn't specify one,
|
|
|
|
|
|
// or this assignment targets a different floor).
|
|
|
|
|
|
$table->string('floor')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
// Door / apartment / unit number override.
|
|
|
|
|
|
$table->string('door')->nullable();
|
|
|
|
|
|
|
2026-04-14 08:20:42 +00:00
|
|
|
|
// ── Extra data ──────────────────────────────────────────
|
|
|
|
|
|
$table->json('meta')->nullable();
|
|
|
|
|
|
|
|
|
|
|
|
$table->timestamps();
|
|
|
|
|
|
|
|
|
|
|
|
// ── Indexes ─────────────────────────────────────────────
|
|
|
|
|
|
$table->index(['assignable_type', 'assignable_id', 'role'], 'addr_assign_owner_role');
|
|
|
|
|
|
$table->index('role');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Reverse the migrations.
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function down(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
Schema::dropIfExists(config('addresses.table_names.address_assignments', 'address_assignments'));
|
|
|
|
|
|
Schema::dropIfExists(config('addresses.table_names.address_links', 'address_links'));
|
|
|
|
|
|
Schema::dropIfExists(config('addresses.table_names.addresses', 'addresses'));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|