1313namespace Geocoder \Provider \Mapbox ;
1414
1515use Geocoder \Collection ;
16- use Geocoder \Exception \InvalidArgument ;
1716use Geocoder \Exception \InvalidServerResponse ;
1817use Geocoder \Exception \UnsupportedOperation ;
1918use 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