Skip to content

Commit 769d0b5

Browse files
committed
feat(mapbox): build requests for the Geocoding API v6
1 parent a23ffaf commit 769d0b5

1 file changed

Lines changed: 132 additions & 83 deletions

File tree

‎src/Provider/Mapbox/Mapbox.php‎

Lines changed: 132 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
namespace Geocoder\Provider\Mapbox;
1414

1515
use Geocoder\Collection;
16-
use Geocoder\Exception\InvalidArgument;
1716
use Geocoder\Exception\InvalidServerResponse;
1817
use Geocoder\Exception\UnsupportedOperation;
1918
use Geocoder\Http\Provider\AbstractHttpProvider;
@@ -30,30 +29,12 @@ final class Mapbox extends AbstractHttpProvider implements Provider
3029
/**
3130
* @var string
3231
*/
33-
public const GEOCODE_ENDPOINT_URL_SSL = 'https://api.mapbox.com/geocoding/v5/%s/%s.json';
32+
public const FORWARD_ENDPOINT_URL = 'https://api.mapbox.com/search/geocode/v6/forward';
3433

3534
/**
3635
* @var string
3736
*/
38-
public const REVERSE_ENDPOINT_URL_SSL = 'https://api.mapbox.com/geocoding/v5/%s/%F,%F.json';
39-
40-
/**
41-
* @var string
42-
*/
43-
public const GEOCODING_MODE_PLACES = 'mapbox.places';
44-
45-
/**
46-
* @var string
47-
*/
48-
public const GEOCODING_MODE_PLACES_PERMANENT = 'mapbox.places-permanent';
49-
50-
/**
51-
* @var string[]
52-
*/
53-
public const GEOCODING_MODES = [
54-
self::GEOCODING_MODE_PLACES,
55-
self::GEOCODING_MODE_PLACES_PERMANENT,
56-
];
37+
public const REVERSE_ENDPOINT_URL = 'https://api.mapbox.com/search/geocode/v6/reverse';
5738

5839
/**
5940
* @var string
@@ -116,16 +97,6 @@ final class Mapbox extends AbstractHttpProvider implements Provider
11697
*/
11798
public const TYPE_SECONDARY_ADDRESS = 'secondary_address';
11899

119-
/**
120-
* @var string
121-
*/
122-
public const TYPE_POI = 'poi';
123-
124-
/**
125-
* @var string
126-
*/
127-
public const TYPE_POI_LANDMARK = 'poi.landmark';
128-
129100
/**
130101
* @var string[]
131102
*/
@@ -137,9 +108,25 @@ final class Mapbox extends AbstractHttpProvider implements Provider
137108
self::TYPE_PLACE,
138109
self::TYPE_LOCALITY,
139110
self::TYPE_NEIGHBORHOOD,
111+
self::TYPE_STREET,
140112
self::TYPE_ADDRESS,
141-
self::TYPE_POI,
142-
self::TYPE_POI_LANDMARK,
113+
];
114+
115+
/**
116+
* Query data keys that trigger the v6 Structured Input mode (the query text is not sent).
117+
*
118+
* @var string[]
119+
*/
120+
public const STRUCTURED_INPUT_FIELDS = [
121+
'address_line1',
122+
'address_number',
123+
'street',
124+
'block',
125+
'place',
126+
'region',
127+
'postcode',
128+
'locality',
129+
'neighborhood',
143130
];
144131

145132
/**
@@ -158,91 +145,143 @@ final class Mapbox extends AbstractHttpProvider implements Provider
158145
private $country;
159146

160147
/**
161-
* @var string
148+
* @var bool
162149
*/
163-
private $geocodingMode;
150+
private $permanent;
164151

165152
/**
166153
* @param ClientInterface $client An HTTP adapter
167154
* @param string $accessToken Your Mapbox access token
155+
* @param string|null $country Restrict results to one or more ISO 3166 alpha-2 countries (comma separated)
156+
* @param bool $permanent Store results permanently (v6 `permanent` parameter)
168157
*/
169158
public function __construct(
170159
ClientInterface $client,
171160
string $accessToken,
172161
?string $country = null,
173-
string $geocodingMode = self::GEOCODING_MODE_PLACES,
162+
bool $permanent = false,
174163
) {
175164
parent::__construct($client);
176165

177-
if (!in_array($geocodingMode, self::GEOCODING_MODES)) {
178-
throw new InvalidArgument('The Mapbox geocoding mode should be either mapbox.places or mapbox.places-permanent.');
179-
}
180-
181166
$this->accessToken = $accessToken;
182167
$this->country = $country;
183-
$this->geocodingMode = $geocodingMode;
168+
$this->permanent = $permanent;
184169
}
185170

186171
public function geocodeQuery(GeocodeQuery $query): Collection
187172
{
188-
// Mapbox API returns invalid data if IP address given
189-
// This API doesn't handle IPs
173+
// The Mapbox API does not geocode raw IP addresses
190174
if (filter_var($query->getText(), FILTER_VALIDATE_IP)) {
191175
throw new UnsupportedOperation('The Mapbox provider does not support IP addresses, only street addresses.');
192176
}
193177

194-
$url = sprintf(self::GEOCODE_ENDPOINT_URL_SSL, $this->geocodingMode, rawurlencode($query->getText()));
178+
$parameters = [];
179+
180+
// v6 Structured Input: typed fields replace the `q` search text
181+
$structured = $this->structuredInputFields($query);
182+
if ([] !== $structured) {
183+
foreach (self::STRUCTURED_INPUT_FIELDS as $field) {
184+
if (isset($structured[$field])) {
185+
$parameters[$field] = $structured[$field];
186+
}
187+
}
188+
} else {
189+
$parameters['q'] = $query->getText();
190+
}
195191

196-
$urlParameters = [];
197192
if ($query->getBounds()) {
198193
// Format is "minLon,minLat,maxLon,maxLat"
199-
$urlParameters['bbox'] = sprintf(
194+
$parameters['bbox'] = sprintf(
200195
'%s,%s,%s,%s',
201-
$query->getBounds()->getWest(),
202-
$query->getBounds()->getSouth(),
203-
$query->getBounds()->getEast(),
204-
$query->getBounds()->getNorth()
196+
$this->formatCoordinate($query->getBounds()->getWest()),
197+
$this->formatCoordinate($query->getBounds()->getSouth()),
198+
$this->formatCoordinate($query->getBounds()->getEast()),
199+
$this->formatCoordinate($query->getBounds()->getNorth())
205200
);
206201
}
207202

208-
if (null !== $locationType = $query->getData('location_type')) {
209-
$urlParameters['types'] = is_array($locationType) ? implode(',', $locationType) : $locationType;
210-
} else {
211-
$urlParameters['types'] = self::DEFAULT_TYPE;
203+
$parameters['types'] = $this->locationTypes($query);
204+
205+
if (null !== $autocomplete = $query->getData('autocomplete')) {
206+
$parameters['autocomplete'] = $autocomplete ? 'true' : 'false';
207+
} elseif ([] !== $structured) {
208+
// Mapbox recommends autocomplete=false for structured input
209+
$parameters['autocomplete'] = 'false';
212210
}
213211

214-
if (null !== $fuzzyMatch = $query->getData('fuzzy_match')) {
215-
$urlParameters['fuzzyMatch'] = $fuzzyMatch ? 'true' : 'false';
212+
if (null !== $proximity = $query->getData('proximity')) {
213+
$parameters['proximity'] = $proximity;
216214
}
217215

218-
if (count($urlParameters) > 0) {
219-
$url .= '?'.http_build_query($urlParameters);
216+
if (null !== $worldview = $query->getData('worldview')) {
217+
$parameters['worldview'] = $worldview;
220218
}
221219

222-
return $this->fetchUrl($url, $query->getLimit(), $query->getLocale(), $query->getData('country', $this->country));
220+
return $this->fetchUrl(
221+
self::FORWARD_ENDPOINT_URL,
222+
$query->getLimit(),
223+
$query->getLocale(),
224+
$query->getData('country', $this->country),
225+
$parameters
226+
);
223227
}
224228

225229
public function reverseQuery(ReverseQuery $query): Collection
226230
{
227231
$coordinate = $query->getCoordinates();
228-
$url = sprintf(
229-
self::REVERSE_ENDPOINT_URL_SSL,
230-
$this->geocodingMode,
231-
$coordinate->getLongitude(),
232-
$coordinate->getLatitude()
232+
$parameters = [
233+
'longitude' => $this->formatCoordinate($coordinate->getLongitude()),
234+
'latitude' => $this->formatCoordinate($coordinate->getLatitude()),
235+
];
236+
237+
$parameters['types'] = $this->locationTypes($query);
238+
239+
return $this->fetchUrl(
240+
self::REVERSE_ENDPOINT_URL,
241+
$query->getLimit(),
242+
$query->getLocale(),
243+
$query->getData('country', $this->country),
244+
$parameters
233245
);
246+
}
234247

235-
if (null !== $locationType = $query->getData('location_type')) {
236-
$urlParameters['types'] = is_array($locationType) ? implode(',', $locationType) : $locationType;
237-
} else {
238-
$urlParameters['types'] = self::DEFAULT_TYPE;
248+
/**
249+
* @return array<string, mixed>
250+
*/
251+
private function structuredInputFields(GeocodeQuery $query): array
252+
{
253+
$fields = [];
254+
255+
foreach (self::STRUCTURED_INPUT_FIELDS as $field) {
256+
$value = $query->getData($field);
257+
if (null !== $value && '' !== $value) {
258+
$fields[$field] = $value;
259+
}
239260
}
240261

241-
if (count($urlParameters) > 0) {
242-
$url .= '?'.http_build_query($urlParameters);
262+
return $fields;
263+
}
264+
265+
/**
266+
* @param GeocodeQuery|ReverseQuery $query
267+
*/
268+
private function locationTypes($query): string
269+
{
270+
if (null !== $locationType = $query->getData('location_type')) {
271+
return is_array($locationType) ? implode(',', $locationType) : $locationType;
243272
}
244273

245-
return $this->fetchUrl($url, $query->getLimit(), $query->getLocale(), $query->getData('country', $this->country));
274+
return self::DEFAULT_TYPE;
275+
}
276+
277+
/**
278+
* Format a coordinate with the shortest decimal representation that round-trips the
279+
* value exactly (a plain (string) cast truncates to the 14 significant digits of the
280+
* default `precision` ini setting, which would change the request URL).
281+
*/
282+
private function formatCoordinate(float $value): string
283+
{
284+
return (string) json_encode($value);
246285
}
247286

248287
public function getName(): string
@@ -251,25 +290,35 @@ public function getName(): string
251290
}
252291

253292
/**
254-
* @return string query with extra params
293+
* @param array<string, mixed> $parameters
255294
*/
256-
private function buildQuery(string $url, int $limit, ?string $locale = null, ?string $country = null): string
295+
private function buildQuery(string $url, int $limit, ?string $locale, ?string $country, array $parameters): string
257296
{
258-
$parameters = array_filter([
259-
'country' => $country,
260-
'language' => $locale,
261-
'limit' => $limit,
262-
'access_token' => $this->accessToken,
263-
]);
297+
if (null !== $country) {
298+
$parameters['country'] = $country;
299+
}
300+
301+
if (null !== $locale) {
302+
$parameters['language'] = $locale;
303+
}
304+
305+
$parameters['limit'] = $limit;
264306

265-
$separator = parse_url($url, PHP_URL_QUERY) ? '&' : '?';
307+
if ($this->permanent) {
308+
$parameters['permanent'] = 'true';
309+
}
310+
311+
$parameters['access_token'] = $this->accessToken;
266312

267-
return $url.$separator.http_build_query($parameters);
313+
return $url.'?'.http_build_query($parameters);
268314
}
269315

270-
private function fetchUrl(string $url, int $limit, ?string $locale = null, ?string $country = null): AddressCollection
316+
/**
317+
* @param array<string, mixed> $parameters
318+
*/
319+
private function fetchUrl(string $url, int $limit, ?string $locale, ?string $country, array $parameters): AddressCollection
271320
{
272-
$url = $this->buildQuery($url, $limit, $locale, $country);
321+
$url = $this->buildQuery($url, $limit, $locale, $country, $parameters);
273322
$content = $this->getUrlContents($url);
274323
$json = $this->validateResponse($url, $content);
275324

0 commit comments

Comments
 (0)