Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Provider | Package | Features | Stats
[GraphHopper](https://github.com/geocoder-php/graphhopper-provider) | `geocoder-php/graphhopper-provider` | address, reverse <br> [Website](https://docs.graphhopper.com/#tag/Geocoding-API) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/graphhopper-provider/v/stable)](https://packagist.org/packages/geocoder-php/graphhopper-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/graphhopper-provider/downloads)](https://packagist.org/packages/geocoder-php/graphhopper-provider)
[Here](https://github.com/geocoder-php/here-provider) | `geocoder-php/here-provider` | address, reverse <br> [Website](https://developer.here.com/documentation/geocoder/topics/quick-start-geocode.html) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/here-provider/v/stable)](https://packagist.org/packages/geocoder-php/here-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/here-provider/downloads)](https://packagist.org/packages/geocoder-php/here-provider)
[LocationIQ](https://github.com/geocoder-php/locationiq-provider) | `geocoder-php/locationiq-provider` | address, reverse <br> [Website](https://locationiq.org/) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/locationiq-provider/v/stable)](https://packagist.org/packages/geocoder-php/locationiq-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/locationiq-provider/downloads)](https://packagist.org/packages/geocoder-php/locationiq-provider)
[Mapbox](https://github.com/geocoder-php/mapbox-provider) | `geocoder-php/mapbox-provider` | address, reverse <br> [Website](https://www.mapbox.com/geocoding/) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/mapbox-provider/v/stable)](https://packagist.org/packages/geocoder-php/mapbox-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/mapbox-provider/downloads)](https://packagist.org/packages/geocoder-php/mapbox-provider)
[Mapbox](https://github.com/geocoder-php/mapbox-provider) | `geocoder-php/mapbox-provider` | address, reverse <br> [Website](https://docs.mapbox.com/api/search/geocoding/) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/mapbox-provider/v/stable)](https://packagist.org/packages/geocoder-php/mapbox-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/mapbox-provider/downloads)](https://packagist.org/packages/geocoder-php/mapbox-provider)
[MapQuest](https://github.com/geocoder-php/mapquest-provider) | `geocoder-php/mapquest-provider` | address, reverse <br> [Website](http://developer.mapquest.com/web/products/dev-services/geocoding-ws) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/mapquest-provider/v/stable)](https://packagist.org/packages/geocoder-php/mapquest-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/mapquest-provider/downloads)](https://packagist.org/packages/geocoder-php/mapquest-provider)
[MapTiler](https://github.com/geocoder-php/maptiler-provider) | `geocoder-php/maptiler-provider` | address, reverse <br> [Website](https://www.maptiler.com/cloud/geocoding/) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/maptiler-provider/v/stable)](https://packagist.org/packages/geocoder-php/maptiler-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/maptiler-provider/downloads)](https://packagist.org/packages/geocoder-php/maptiler-provider)
[Nominatim](https://github.com/geocoder-php/nominatim-provider) <br> (OpenStreetMap) | `geocoder-php/nominatim-provider` | address, reverse <br> [Website](http://wiki.openstreetmap.org/wiki/Nominatim) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/nominatim-provider/v/stable)](https://packagist.org/packages/geocoder-php/nominatim-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/nominatim-provider/downloads)](https://packagist.org/packages/geocoder-php/nominatim-provider)
Expand Down
20 changes: 20 additions & 0 deletions src/Provider/Mapbox/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release.

## 2.0.0

### Added

- Support for the Mapbox Geocoding API v6 (`/search/geocode/v6/forward` and `/search/geocode/v6/reverse`)
- Support for the v6 `autocomplete` parameter and the v6 Structured Input fields
- `Mapbox::TYPE_STREET` and `Mapbox::STRUCTURED_INPUT_FIELDS` constants
- `MapboxAddress::getMatchCode()`, `getMatchConfidence()` and `getAccuracy()` for the v6 response data

### Removed

- Support for the Mapbox Geocoding API v5 (pin to `^1.5` to keep using it)
- Support for the v5 `fuzzy_match` parameter (use `autocomplete` instead)
- `Mapbox::TYPE_POI` and `Mapbox::TYPE_POI_LANDMARK` constants (not available in v6)

### Changed

- The `mapbox.places-permanent` mode is now sent as the v6 `permanent` parameter
- Results carry the v6 `mapbox_id` format and v6 dataset values

## 1.5.0

### Added
Expand Down
221 changes: 151 additions & 70 deletions src/Provider/Mapbox/Mapbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ final class Mapbox extends AbstractHttpProvider implements Provider
/**
* @var string
*/
public const GEOCODE_ENDPOINT_URL_SSL = 'https://api.mapbox.com/geocoding/v5/%s/%s.json';
public const GEOCODE_ENDPOINT_URL_SSL = 'https://api.mapbox.com/search/geocode/v6/forward';

/**
* @var string
*/
public const REVERSE_ENDPOINT_URL_SSL = 'https://api.mapbox.com/geocoding/v5/%s/%F,%F.json';
public const REVERSE_ENDPOINT_URL_SSL = 'https://api.mapbox.com/search/geocode/v6/reverse';

/**
* @var string
Expand Down Expand Up @@ -98,12 +98,7 @@ final class Mapbox extends AbstractHttpProvider implements Provider
/**
* @var string
*/
public const TYPE_POI = 'poi';

/**
* @var string
*/
public const TYPE_POI_LANDMARK = 'poi.landmark';
public const TYPE_STREET = 'street';

/**
* @var string[]
Expand All @@ -116,16 +111,33 @@ final class Mapbox extends AbstractHttpProvider implements Provider
self::TYPE_PLACE,
self::TYPE_LOCALITY,
self::TYPE_NEIGHBORHOOD,
self::TYPE_STREET,
self::TYPE_ADDRESS,
self::TYPE_POI,
self::TYPE_POI_LANDMARK,
];

/**
* @var string
*/
public const DEFAULT_TYPE = self::TYPE_ADDRESS;

/**
* v6 Structured Input fields. When one or more of these is set on the
* query, the typed fields are sent and the `q` search text is dropped.
*
* @var string[]
*/
public const STRUCTURED_INPUT_FIELDS = [
'address_line1',
'address_number',
'street',
'block',
'place',
'region',
'postcode',
'locality',
'neighborhood',
];

/**
* @var string
*/
Expand All @@ -142,8 +154,11 @@ final class Mapbox extends AbstractHttpProvider implements Provider
private $geocodingMode;

/**
* @param ClientInterface $client An HTTP adapter
* @param string $accessToken Your Mapbox access token
* @param ClientInterface $client An HTTP adapter
* @param string $accessToken Your Mapbox access token
* @param string|null $country Restrict the results to one or more ISO 3166 alpha-2 countries
* @param string $geocodingMode One of the GEOCODING_MODES; v6 expresses the permanent mode
* through the `permanent` request parameter
*/
public function __construct(
ClientInterface $client,
Expand All @@ -170,17 +185,30 @@ public function geocodeQuery(GeocodeQuery $query): Collection
throw new UnsupportedOperation('The Mapbox provider does not support IP addresses, only street addresses.');
}

$url = sprintf(self::GEOCODE_ENDPOINT_URL_SSL, $this->geocodingMode, rawurlencode($query->getText()));
$url = self::GEOCODE_ENDPOINT_URL_SSL;

$urlParameters = [];

// v6 Structured Input: the typed fields replace the `q` search text
$structured = $this->structuredInputFields($query);
if ([] !== $structured) {
foreach (self::STRUCTURED_INPUT_FIELDS as $field) {
if (isset($structured[$field])) {
$urlParameters[$field] = $structured[$field];
}
}
} else {
$urlParameters['q'] = $query->getText();
}

if ($query->getBounds()) {
// Format is "minLon,minLat,maxLon,maxLat"
$urlParameters['bbox'] = sprintf(
'%s,%s,%s,%s',
$query->getBounds()->getWest(),
$query->getBounds()->getSouth(),
$query->getBounds()->getEast(),
$query->getBounds()->getNorth()
$this->formatCoordinate($query->getBounds()->getWest()),
$this->formatCoordinate($query->getBounds()->getSouth()),
$this->formatCoordinate($query->getBounds()->getEast()),
$this->formatCoordinate($query->getBounds()->getNorth())
);
}

Expand All @@ -190,8 +218,12 @@ public function geocodeQuery(GeocodeQuery $query): Collection
$urlParameters['types'] = self::DEFAULT_TYPE;
}

if (null !== $fuzzyMatch = $query->getData('fuzzy_match')) {
$urlParameters['fuzzyMatch'] = $fuzzyMatch ? 'true' : 'false';
// v6 has no `fuzzyMatch` parameter; `autocomplete` is its closest replacement
if (null !== $autocomplete = $query->getData('autocomplete')) {
$urlParameters['autocomplete'] = $autocomplete ? 'true' : 'false';
} elseif ([] !== $structured) {
// Mapbox recommends disabling autocomplete for structured input
$urlParameters['autocomplete'] = 'false';
}

if (count($urlParameters) > 0) {
Expand All @@ -204,12 +236,12 @@ public function geocodeQuery(GeocodeQuery $query): Collection
public function reverseQuery(ReverseQuery $query): Collection
{
$coordinate = $query->getCoordinates();
$url = sprintf(
self::REVERSE_ENDPOINT_URL_SSL,
$this->geocodingMode,
$coordinate->getLongitude(),
$coordinate->getLatitude()
);
$url = self::REVERSE_ENDPOINT_URL_SSL;

$urlParameters = [
'longitude' => $this->formatCoordinate($coordinate->getLongitude()),
'latitude' => $this->formatCoordinate($coordinate->getLatitude()),
];

if (null !== $locationType = $query->getData('location_type')) {
$urlParameters['types'] = is_array($locationType) ? implode(',', $locationType) : $locationType;
Expand Down Expand Up @@ -237,10 +269,17 @@ private function buildQuery(string $url, int $limit, ?string $locale = null, ?st
$parameters = array_filter([
'country' => $country,
'language' => $locale,
'limit' => $limit,
'access_token' => $this->accessToken,
]);

$parameters['limit'] = $limit;

if (self::GEOCODING_MODE_PLACES_PERMANENT === $this->geocodingMode) {
// v6 replaced the "mapbox.places-permanent" mode with a `permanent` parameter
$parameters['permanent'] = 'true';
}

$parameters['access_token'] = $this->accessToken;

$separator = parse_url($url, PHP_URL_QUERY) ? '&' : '?';

return $url.$separator.http_build_query($parameters);
Expand All @@ -259,42 +298,64 @@ private function fetchUrl(string $url, int $limit, ?string $locale = null, ?stri

$results = [];
foreach ($json['features'] as $result) {
if (!array_key_exists('context', $result)) {
break;
}

$builder = new AddressBuilder($this->getName());
$this->parseCoordinates($builder, $result);

// set official Mapbox place id
if (isset($result['id'])) {
$builder->setValue('id', $result['id']);
}
// in v6 the feature details (id, name, type, context) live in `properties`
$properties = $result['properties'] ?? [];

// set official Mapbox place id
if (isset($result['text'])) {
$builder->setValue('street_name', $result['text']);
if (isset($properties['mapbox_id'])) {
$builder->setValue('id', $properties['mapbox_id']);
} elseif (isset($result['id'])) {
$builder->setValue('id', $result['id']);
}

// update address components
foreach ($result['context'] as $component) {
$this->updateAddressComponent($builder, $component['id'], $component);
// update address components (v6 `context` is an object keyed by feature type)
foreach ($properties['context'] ?? [] as $type => $component) {
$this->updateAddressComponent($builder, (string) $type, $component);
}

/** @var MapboxAddress $address */
$address = $builder->build(MapboxAddress::class);
$address = $address->withId($builder->getValue('id'));
if (isset($result['address'])) {
$address = $address->withStreetNumber($result['address']);

// street name without the house number
$streetName = $properties['context']['address']['street_name']
?? $properties['context']['street']['name']
?? $properties['name']
?? null;
if (null !== $streetName) {
$address = $address->withStreetName((string) $streetName);
}
if (isset($result['place_type'])) {
$address = $address->withResultType($result['place_type']);

if (isset($properties['context']['address']['address_number'])) {
$address = $address->withStreetNumber((string) $properties['context']['address']['address_number']);
}
if (isset($result['place_name'])) {
$address = $address->withFormattedAddress($result['place_name']);

if (isset($properties['feature_type'])) {
$address = $address->withResultType([(string) $properties['feature_type']]);
}
$address = $address->withStreetName($builder->getValue('street_name'));
$address = $address->withNeighborhood($builder->getValue('neighborhood'));

if (isset($properties['full_address'])) {
$address = $address->withFormattedAddress((string) $properties['full_address']);
}

if (isset($properties['context']['neighborhood']['name'])) {
$address = $address->withNeighborhood((string) $properties['context']['neighborhood']['name']);
}

if (isset($properties['match_code']) && is_array($properties['match_code'])) {
$address = $address->withMatchCode($properties['match_code']);
if (isset($properties['match_code']['confidence'])) {
$address = $address->withMatchConfidence((string) $properties['match_code']['confidence']);
}
}

if (isset($properties['coordinates']['accuracy'])) {
$address = $address->withAccuracy((string) $properties['coordinates']['accuracy']);
}

$results[] = $address;

if (count($results) >= $limit) {
Expand All @@ -305,57 +366,77 @@ private function fetchUrl(string $url, int $limit, ?string $locale = null, ?stri
return new AddressCollection($results);
}

/**
* @return array<string, mixed>
*/
private function structuredInputFields(GeocodeQuery $query): array
{
$fields = [];

foreach (self::STRUCTURED_INPUT_FIELDS as $field) {
$value = $query->getData($field);
if (null !== $value && '' !== $value) {
$fields[$field] = $value;
}
}

return $fields;
}

/**
* Format a coordinate with the shortest representation that round-trips
* the value exactly (a plain (string) cast would truncate it to the
* `precision` ini setting).
*/
private function formatCoordinate(float $value): string
{
return (string) json_encode($value);
}

/**
* Update current resultSet with given key/value.
*
* @param string $type Component type
* @param array<string, mixed> $value Component value
* @param array<string, mixed> $component
*/
private function updateAddressComponent(AddressBuilder $builder, string $type, array $value): void
private function updateAddressComponent(AddressBuilder $builder, string $type, array $component): void
{
$typeParts = explode('.', $type);
$type = reset($typeParts);
if (!isset($component['name'])) {
return;
}

switch ($type) {
case 'postcode':
$builder->setPostalCode($value['text']);
$builder->setPostalCode($component['name']);

break;

case 'locality':
$builder->setLocality($value['text']);
$builder->setLocality($component['name']);

break;

case 'country':
$builder->setCountry($value['text']);
if (isset($value['short_code'])) {
$builder->setCountryCode(strtoupper($value['short_code']));
$builder->setCountry($component['name']);
if (isset($component['country_code'])) {
$builder->setCountryCode(strtoupper((string) $component['country_code']));
}

break;

case 'neighborhood':
$builder->setValue($type, $value['text']);

break;

case 'place':
$builder->addAdminLevel(1, $value['text']);
$builder->setLocality($value['text']);
$builder->addAdminLevel(1, $component['name']);
$builder->setLocality($component['name']);

break;

case 'region':
$code = null;
if (!empty($value['short_code']) && preg_match('/[A-z]{2}-/', $value['short_code'])) {
$code = preg_replace('/[A-z]{2}-/', '', $value['short_code']);
}
$builder->addAdminLevel(2, $value['text'], $code);
$code = isset($component['region_code']) ? (string) $component['region_code'] : null;
$builder->addAdminLevel(2, $component['name'], $code);

break;

default:
// address, street, district: handled elsewhere or not part of the address model
}
}

Expand Down
Loading