329 lines
8.7 KiB
Markdown
329 lines
8.7 KiB
Markdown
# AddressService
|
||
|
||
The `AddressService` is a singleton service providing distance calculations, proximity queries, duplicate detection, formatting and coordinate conversion.
|
||
|
||
## Accessing the Service
|
||
|
||
```php
|
||
// Via the global helper
|
||
address()->distanceBetween($a, $b);
|
||
|
||
// Via dependency injection
|
||
use Blax\Addresses\Services\AddressService;
|
||
|
||
public function __construct(private AddressService $addressService) {}
|
||
|
||
// Via the container
|
||
app(AddressService::class)->nearby($lat, $lng, 10);
|
||
```
|
||
|
||
---
|
||
|
||
## Distance Calculation
|
||
|
||
### `distanceBetween(Address $from, Address $to, string $unit = 'km'): ?float`
|
||
|
||
Calculate the great-circle distance between two addresses using the Haversine formula.
|
||
|
||
```php
|
||
$vienna = Address::create(['latitude' => 48.2082, 'longitude' => 16.3738]);
|
||
$berlin = Address::create(['latitude' => 52.5200, 'longitude' => 13.4050]);
|
||
|
||
$km = address()->distanceBetween($vienna, $berlin); // ~524.2 km
|
||
$mi = address()->distanceBetween($vienna, $berlin, 'mi'); // ~325.8 mi
|
||
```
|
||
|
||
Returns `null` if either address is missing coordinates.
|
||
|
||
### `haversine(float $lat1, float $lng1, float $lat2, float $lng2, string $unit = 'km'): float`
|
||
|
||
Calculate distance directly from coordinate pairs — no Address models needed.
|
||
|
||
```php
|
||
$distance = address()->haversine(48.2082, 16.3738, 52.5200, 13.4050); // ~524.2 km
|
||
```
|
||
|
||
### `altitudeDifference(Address $from, Address $to): ?float`
|
||
|
||
Calculate the altitude difference in metres (signed: `to − from`).
|
||
|
||
```php
|
||
$valley = Address::create(['altitude' => 200.0, 'latitude' => 0, 'longitude' => 0]);
|
||
$peak = Address::create(['altitude' => 1800.0, 'latitude' => 0, 'longitude' => 0]);
|
||
|
||
address()->altitudeDifference($valley, $peak); // 1600.0
|
||
address()->altitudeDifference($peak, $valley); // -1600.0
|
||
```
|
||
|
||
Returns `null` if either address is missing altitude data.
|
||
|
||
### Constants
|
||
|
||
| Constant | Value | Description |
|
||
|-----------------------------------|--------|---------------------------------|
|
||
| `AddressService::EARTH_RADIUS_KM` | 6371.0 | Mean Earth radius in kilometres |
|
||
| `AddressService::EARTH_RADIUS_MI` | 3958.8 | Mean Earth radius in miles |
|
||
|
||
---
|
||
|
||
## Proximity Queries
|
||
|
||
### `nearby(float $latitude, float $longitude, float $radius, string $unit = 'km'): Collection`
|
||
|
||
Find all addresses within a given radius of a coordinate point. Uses a bounding-box pre-filter for performance, then refines with Haversine. Results are ordered by distance (nearest first).
|
||
|
||
```php
|
||
// All addresses within 10 km of St. Stephen's Cathedral
|
||
$nearby = address()->nearby(48.2082, 16.3738, 10);
|
||
|
||
foreach ($nearby as $address) {
|
||
echo $address->city; // "Vienna"
|
||
echo $address->distance; // 2.34 (km from centre)
|
||
}
|
||
```
|
||
|
||
Each returned `Address` has a `->distance` attribute appended with the calculated distance.
|
||
|
||
### `nearbyAddress(Address $address, float $radius, string $unit = 'km', bool $excludeSelf = true): Collection`
|
||
|
||
Convenience wrapper — find addresses near a given address.
|
||
|
||
```php
|
||
$office = Address::create([
|
||
'street' => 'Stephansplatz 1',
|
||
'city' => 'Vienna',
|
||
'latitude' => 48.2082,
|
||
'longitude' => 16.3738,
|
||
]);
|
||
|
||
// Find other addresses within 5 km
|
||
$neighbours = address()->nearbyAddress($office, 5);
|
||
|
||
// Include the reference address itself
|
||
$all = address()->nearbyAddress($office, 5, 'km', false);
|
||
```
|
||
|
||
Returns an empty collection if the address has no coordinates.
|
||
|
||
### `closest(float $latitude, float $longitude): ?Address`
|
||
|
||
Get the single closest address to a coordinate point.
|
||
|
||
```php
|
||
$nearest = address()->closest(48.2082, 16.3738);
|
||
|
||
echo $nearest->formatted; // "Stephansplatz 1, 1010, Vienna, AT"
|
||
echo $nearest->distance; // 0.12 (km)
|
||
```
|
||
|
||
Returns `null` if no addresses with coordinates exist.
|
||
|
||
---
|
||
|
||
## Bounding Box
|
||
|
||
### `boundingBox(float $latitude, float $longitude, float $radius, string $unit = 'km'): array`
|
||
|
||
Calculate a latitude/longitude bounding box around a centre point. Useful as a fast pre-filter before computing Haversine distances.
|
||
|
||
```php
|
||
$box = address()->boundingBox(48.2082, 16.3738, 10); // 10 km radius
|
||
|
||
// Returns:
|
||
// [
|
||
// 'minLat' => 48.1183...,
|
||
// 'maxLat' => 48.2981...,
|
||
// 'minLng' => 16.2395...,
|
||
// 'maxLng' => 16.5081...,
|
||
// ]
|
||
|
||
// Use in a query
|
||
Address::whereBetween('latitude', [$box['minLat'], $box['maxLat']])
|
||
->whereBetween('longitude', [$box['minLng'], $box['maxLng']])
|
||
->get();
|
||
```
|
||
|
||
---
|
||
|
||
## Duplicate Detection & Merging
|
||
|
||
### `findDuplicates(Address $address): Collection`
|
||
|
||
Find addresses that look like potential duplicates. Matches on `street`, `postal_code`, `city` and `country_code`.
|
||
|
||
```php
|
||
$address = Address::create([
|
||
'street' => 'Baker Street 221B',
|
||
'postal_code' => 'NW1 6XE',
|
||
'city' => 'London',
|
||
'country_code' => 'GB',
|
||
]);
|
||
|
||
$duplicates = address()->findDuplicates($address);
|
||
|
||
foreach ($duplicates as $dup) {
|
||
echo "Possible duplicate: #{$dup->id} — {$dup->formatted}";
|
||
}
|
||
```
|
||
|
||
### `merge(Address $target, Address $duplicate): int`
|
||
|
||
Merge a duplicate address into a target. All `AddressLink` rows pointing to the duplicate are reassigned to the target, and the duplicate is soft-deleted.
|
||
|
||
```php
|
||
$target = Address::find(1); // the one to keep
|
||
$duplicate = Address::find(2); // the one to merge away
|
||
|
||
$reassigned = address()->merge($target, $duplicate);
|
||
echo "Reassigned {$reassigned} links";
|
||
|
||
$duplicate->trashed(); // true
|
||
```
|
||
|
||
---
|
||
|
||
## Query Builders
|
||
|
||
These methods return Eloquent `Builder` instances for further chaining.
|
||
|
||
### `inCountry(string $countryCode): Builder`
|
||
|
||
```php
|
||
$austrianAddresses = address()->inCountry('AT')->get();
|
||
$austrianCount = address()->inCountry('AT')->count();
|
||
```
|
||
|
||
### `inCity(string $city, ?string $countryCode = null): Builder`
|
||
|
||
```php
|
||
$viennaAddresses = address()->inCity('Vienna')->get();
|
||
|
||
// Disambiguate: Vienna, Austria vs Vienna, Virginia
|
||
$at = address()->inCity('Vienna', 'AT')->get();
|
||
```
|
||
|
||
### `inPostalCode(string $postalCode, ?string $countryCode = null): Builder`
|
||
|
||
```php
|
||
$addresses = address()->inPostalCode('1010')->get();
|
||
$addresses = address()->inPostalCode('1010', 'AT')->get();
|
||
```
|
||
|
||
### `withCoordinates(): Builder`
|
||
|
||
Get all addresses that have latitude and longitude set.
|
||
|
||
```php
|
||
$geoAddresses = address()->withCoordinates()->get();
|
||
$count = address()->withCoordinates()->count();
|
||
```
|
||
|
||
---
|
||
|
||
## Formatting
|
||
|
||
### `format(Address $address, string $separator = ', '): string`
|
||
|
||
Build a single-line formatted string from an address.
|
||
|
||
```php
|
||
$address = Address::create([
|
||
'street' => '350 Fifth Avenue',
|
||
'building' => 'Empire State Building',
|
||
'floor' => '32',
|
||
'postal_code' => '10118',
|
||
'city' => 'New York',
|
||
'state' => 'NY',
|
||
'country_code' => 'US',
|
||
]);
|
||
|
||
echo address()->format($address);
|
||
// "350 Fifth Avenue, (Empire State Building), Floor 32, 10118, New York, NY, US"
|
||
|
||
echo address()->format($address, ' | ');
|
||
// "350 Fifth Avenue | (Empire State Building) | Floor 32 | 10118 | New York | NY | US"
|
||
```
|
||
|
||
> **Tip:** The `Address` model also has a `$address->formatted` accessor that produces the same single-line output with `", "` separator.
|
||
|
||
### `formatMultiline(Address $address): string`
|
||
|
||
Build a multi-line, postal-style formatted string.
|
||
|
||
```php
|
||
echo address()->formatMultiline($address);
|
||
// 350 Fifth Avenue
|
||
// Empire State Building, Floor 32
|
||
// 10118 New York, NY
|
||
// US
|
||
```
|
||
|
||
Line structure:
|
||
1. Street + street_extra
|
||
2. Building, floor, room
|
||
3. Postal code + city, state
|
||
4. County (if set)
|
||
5. Country code
|
||
|
||
### `formatCoordinates(Address $address): ?string`
|
||
|
||
Format coordinates as a human-readable string.
|
||
|
||
```php
|
||
$address = Address::create([
|
||
'latitude' => 48.2082,
|
||
'longitude' => 16.3738,
|
||
'altitude' => 171.0,
|
||
]);
|
||
|
||
echo address()->formatCoordinates($address);
|
||
// "48.2082000°N, 16.3738000°E (alt: 171.00m AMSL)"
|
||
```
|
||
|
||
Returns `null` if no coordinates are set.
|
||
|
||
---
|
||
|
||
## Coordinate Conversion
|
||
|
||
### `dmsToDecimal(int $degrees, int $minutes, float $seconds, string $direction): float`
|
||
|
||
Convert degrees/minutes/seconds (DMS) to decimal degrees.
|
||
|
||
```php
|
||
// 48° 12' 29.52" N
|
||
$lat = address()->dmsToDecimal(48, 12, 29.52, 'N'); // 48.2082
|
||
|
||
// 16° 22' 25.68" E
|
||
$lng = address()->dmsToDecimal(16, 22, 25.68, 'E'); // 16.3738
|
||
|
||
// Southern / Western hemispheres yield negative values
|
||
$lat = address()->dmsToDecimal(33, 51, 54.0, 'S'); // -33.865
|
||
```
|
||
|
||
### `decimalToDms(float $decimal, string $axis = 'lat'): array`
|
||
|
||
Convert decimal degrees to DMS.
|
||
|
||
```php
|
||
$dms = address()->decimalToDms(48.2082, 'lat');
|
||
// [
|
||
// 'degrees' => 48,
|
||
// 'minutes' => 12,
|
||
// 'seconds' => 29.52,
|
||
// 'direction' => 'N',
|
||
// ]
|
||
|
||
$dms = address()->decimalToDms(-73.9854, 'lng');
|
||
// [
|
||
// 'degrees' => 73,
|
||
// 'minutes' => 59,
|
||
// 'seconds' => 7.44,
|
||
// 'direction' => 'W',
|
||
// ]
|
||
```
|
||
|
||
The `$axis` parameter determines the direction letter:
|
||
- `'lat'` → N (positive) / S (negative)
|
||
- `'lng'` → E (positive) / W (negative)
|