From e67a4c1069c5094ecfe1e80576623bc2abdc7938 Mon Sep 17 00:00:00 2001 From: David Barratt Date: Fri, 28 Aug 2026 16:52:25 +0000 Subject: [PATCH 01/13] docs: add design and implementation plan for Mapbox Geocoding v6 --- docs/plans/2026-08-28-mapbox-v6-design.md | 140 ++ docs/plans/2026-08-28-mapbox-v6.md | 1816 +++++++++++++++++++++ 2 files changed, 1956 insertions(+) create mode 100644 docs/plans/2026-08-28-mapbox-v6-design.md create mode 100644 docs/plans/2026-08-28-mapbox-v6.md diff --git a/docs/plans/2026-08-28-mapbox-v6-design.md b/docs/plans/2026-08-28-mapbox-v6-design.md new file mode 100644 index 000000000..5ba52cf75 --- /dev/null +++ b/docs/plans/2026-08-28-mapbox-v6-design.md @@ -0,0 +1,140 @@ +# Mapbox Geocoding v6 — Design + +Issue: https://github.com/geocoder-php/Geocoder/issues/1237 +Date: 2026-08-28 +Status: validated + +## Background + +Mapbox released the [Geocoding API v6](https://docs.mapbox.com/api/search/geocoding/) (GA). The v6 +API is more precise, adds **Structured Input** (typed address fields instead of free text), +**Smart Address Match** (`match_code`), point `accuracy`, `routable_points`, and new feature +types. The v5 API keeps running; Mapbox does not force a migration. + +## Decision + +**Upgrade the existing `Geocoder\Provider\Mapbox\Mapbox` provider in place, as a major +version of `geocoder-php/mapbox-provider` (1.x → 2.0.0).** All breaking changes are listed +in the provider CHANGELOG. Users who need v5 pin to `^1.5`. + +Rationale: the issue asks for *the* Mapbox provider to support v6; the repo's deprecation +procedure (`docs/procedures/provider-deprecation.md`) only applies once an API stops +responding, which has not happened for v5 — but the provider may move forward in a major +release. (A parallel `MapboxV6` class was considered and rejected in favor of a clean +major bump.) + +## Verified v6 API behavior (probed against the live API) + +- Forward: `GET https://api.mapbox.com/search/geocode/v6/forward?q={text}` (or structured fields, no `q`) +- Reverse: `GET https://api.mapbox.com/search/geocode/v6/reverse?longitude={lon}&latitude={lat}` +- Invalid token → **401** (v5 used 403). Both map to `InvalidCredentials` in `AbstractHttpProvider`. +- `types=poi` / `types=poi.landmark` → **422** (POI geocoding removed in v6). +- `types` valid values (per the API's own 422 message): `country, region, postcode, district, + place, locality, neighborhood, street, block, address, secondary_address`. `block` is only + accepted in Japan context (`country=jp`), `secondary_address` in US context; the docs list + the 9 globally-safe values. +- Forward `limit=11` → 200 (silently clamped to the max of 10). +- Reverse `limit>1` **requires exactly one** `types` value, else 422. +- `autocomplete` (replaces v5 `fuzzyMatch`, inverted default): prefix-style matching; a typo'd + query (`wahsington`) returns nothing in v6 — there is no typo tolerance; that is what + `match_code` confidence is for. +- Structured Input: `q` is dropped in favor of `address_line1`, `address_number`, `street`, + `block`, `place`, `region`, `postcode`, `locality`, `neighborhood` (plus `country`); Mapbox + recommends `autocomplete=false` for structured input. +- Response: GeoJSON. `properties` contains `mapbox_id`, `feature_type` (string, formerly + `place_type` array), `name`, `full_address`, `coordinates{longitude,latitude,accuracy, + routable_points}`, `match_code{... ,confidence}` (forward only) and `context` as an + **object keyed by feature type** (v5 was an array of `{id,text,short_code}`). + `context.country` uses `country_code`, `context.region` uses `region_code`/`region_code_full`, + `context.address` uses `street_name`/`address_number`. +- `bbox` is documented on non-address features but is **not currently returned** in any of the + probed responses; the parser maps it defensively when present. + +## Public API surface (2.0.0) + +### Constructor (breaking) + +```php +new Mapbox(ClientInterface $client, string $accessToken, ?string $country = null, bool $permanent = false) +``` + +The 4th argument changes from `string $geocodingMode` to `bool $permanent` (v6 `permanent=true` +storage, applied to forward and reverse requests). + +### Constants + +- `FORWARD_ENDPOINT_URL`, `REVERSE_ENDPOINT_URL` (replace `GEOCODE_ENDPOINT_URL_SSL` / + `REVERSE_ENDPOINT_URL_SSL`). +- Types: `country, region, postcode, district, place, locality, neighborhood, street, address` + in `TYPES` (adds `TYPE_STREET`; removes `TYPE_POI` / `TYPE_POI_LANDMARK`). +- `TYPE_BLOCK` (Japan) and `TYPE_SECONDARY_ADDRESS` (US) exposed as constants but **not** in + `TYPES` (conditionally accepted by the API only). +- `STRUCTURED_INPUT_FIELDS`: the data keys that trigger Structured Input. +- Removed: `GEOCODING_MODE_PLACES`, `GEOCODING_MODE_PLACES_PERMANENT`, `GEOCODING_MODES`. + +### Query data keys + +| Key | v6 param | Note | +|---|---|---| +| `location_type` (string\|array) | `types` | kept from v5; default `address` | +| `autocomplete` (bool) | `autocomplete` | replaces `fuzzy_match` (removed) | +| `country` (string) | `country` | kept; also a structured field | +| `proximity` (`"lon,lat"`\|`"ip"`) | `proximity` | new | +| `worldview` (string) | `worldview` | new | +| `address_line1`, `address_number`, `street`, `block`, `place`, `region`, `postcode`, `locality`, `neighborhood` | same | structured input | +| `bounds` → `bbox`, `locale` → `language`, `limit` → `limit` | | kept | + +**Mode selection:** if any structured field is present, `q` is not sent and `autocomplete` +defaults to `false` (unless the user sets it). Otherwise `q` = query text. (The text must +still be non-empty because `GeocodeQuery::create('')` throws; it is simply not sent in +structured mode.) + +### Parameter order (deterministic URLs) + +- Forward: `q` | structured fields (canonical order) → `bbox` → `types` → `autocomplete` → + `proximity` → `worldview` → `country` → `language` → `limit` → `permanent` (if true) → + `access_token` +- Reverse: `longitude` → `latitude` → `types` → `country` → `language` → `limit` → + `permanent` (if true) → `access_token` + +### Response → model mapping + +| Model | v6 source | +|---|---| +| `id` | `properties.mapbox_id` (fallback: feature `id`) | +| `streetName` | `context.address.street_name` → `context.street.name` → `properties.name` (keeps v5 semantics: no house number) | +| `streetNumber` | `context.address.address_number` | +| `formattedAddress` | `properties.full_address` | +| `neighborhood` | `context.neighborhood.name` | +| `resultType` | `[properties.feature_type]` (array kept for BC) | +| `postalCode` / `locality` / `country` (+code) / admin levels | `context.postcode.name`, `context.locality.name` + `place.name` (→ locality + level 1), `context.region.name` + `region_code` (→ level 2), `context.country.name` + `country_code` — same precedence rules as v5 (later context entry wins for locality) | +| `bounds` | feature `bbox` (when present) | +| **new** `matchCode` / `matchConfidence` | `properties.match_code` / `.confidence` | +| **new** `accuracy` | `properties.coordinates.accuracy` | + +Errors/behavior: IP text → `UnsupportedOperation` (unchanged); 401/403 → `InvalidCredentials`, +429 → `QuotaExceeded`, other ≥300 → `InvalidServerResponse` (base class, unchanged); +`getName()` stays `mapbox`; client-side result limit kept. + +## Testing strategy (TDD) + +1. **Mocked unit tests** — no network: + - basics: name, IP rejection ×3, 429, 401, empty reverse response; + - exact-URL assertions via `getMockedHttpClientCallback` (forward basic / all options / + structured / explicit autocomplete override / reverse / reverse with types+country); + - response-parsing tests with inline JSON fixtures (address feature, place feature, street + feature, feature without context, empty features, invalid JSON). +2. **Integration tests with cached responses** (repo convention): 7 real v6 queries recorded + with a real key (see implementation plan for the exact URLs); `CachedResponseClient` + masks the key as `[apikey]` so CI replays the fixtures with the placeholder key. + v5 fixtures are deleted. +3. `composer test` (full suite), `composer analyse` (PHPStan L6), `composer cs` — per + `CONTRIBUTING.md`. + +## Out of scope (YAGNI) + +- Batch geocoding endpoint (`/v6/batch`) +- `entrances` (public preview) +- `routable_points` on the model +- `format=v5` compatibility mode (we parse native v6) +- Deprecation of the v5 API (it still works; no maintainer decision to deprecate) diff --git a/docs/plans/2026-08-28-mapbox-v6.md b/docs/plans/2026-08-28-mapbox-v6.md new file mode 100644 index 000000000..d0a3724d8 --- /dev/null +++ b/docs/plans/2026-08-28-mapbox-v6.md @@ -0,0 +1,1816 @@ +# Mapbox Geocoding v6 Implementation Plan + +> **REQUIRED SUB-SKILL:** Use the executing-plans skill to implement this plan task-by-task. + +**Goal:** Upgrade `Geocoder\Provider\Mapbox\Mapbox` in place to the Mapbox Geocoding API v6 (forward, reverse, Structured Input), as a major version of `geocoder-php/mapbox-provider`, with all breaking changes documented in its CHANGELOG. + +**Architecture:** The provider keeps its class name, `Provider`/`AbstractHttpProvider` contract, and the `MapboxAddress` model; request building targets the new `/search/geocode/v6/forward` and `/search/geocode/v6/reverse` endpoints, and response parsing maps the v6 GeoJSON `properties`/`context` object onto the existing model (plus new `matchCode`/`matchConfidence`/`accuracy` getters). Tests follow the repo convention: mocked URL-construction + parsing tests plus integration tests replayed from recorded `.cached_responses` fixtures. + +**Tech Stack:** PHP >= 8.2, PHPUnit 9.6, PSR-18 client, `geocoder-php/provider-integration-tests` (`CachedResponseClient`), PHPStan level 6, PHP-CS-Fixer. + +**Design doc:** `docs/plans/2026-08-28-mapbox-v6-design.md` (read it first). + +**Working branch:** `mapbox-v6` (off `master`). All commands run from the repo root +`/Users/david/Sites/geocoder-php/Geocoder` unless stated otherwise. + +**Test command shorthand:** `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` +(add `--filter ` for single tests). The suite runs with +`USE_CACHED_RESPONSES=true` and the placeholder key from `phpunit.xml.dist`. + +**Live API key:** available in the environment as `MAPBOX_GEOCODING_KEY` (only needed if +fixtures must be re-recorded in Task 0; all tests run without it). + +--- + +### Task 0: Baseline + record v6 fixtures (fixtures are test *inputs*, not implementation) + +**Files:** +- Modify (working tree only, committed in Task 9): `src/Provider/Mapbox/Tests/.cached_responses/` + +**Step 0.1: Verify the current (v5) suite passes** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` +Expected: all tests PASS (cached v5 responses). If vendor is missing, run `composer install` first. + +**Step 0.2: Verify the 7 recorded v6 fixtures are present** + +The fixture files were recorded on 2026-08-28 with the live API (working tree: 7 untracked +`api.mapbox.com_*` files in `src/Provider/Mapbox/Tests/.cached_responses/`, plus 3 v5 files +deleted). Verify exactly these names exist: + +``` +api.mapbox.com_06551ee827f3b9635793737514ec6b9d368075c4 (T7 structured, Park City UT) +api.mapbox.com_338a05e2ceeb9a75e55774746945065683127870 (T5 autocomplete=true, "washi") +api.mapbox.com_5e53474a50aa75267953e6d90b2e3472021efbf2 (T1 "149 9th St, San Francisco, CA 94103") +api.mapbox.com_7b8b47952b295102acd4cb1bc0a9536c26c7bc9b (T3 reverse 2.388911,48.8631507) +api.mapbox.com_9e3e5555aca6117bf14dbd86c37ed23cbece335d (T6 autocomplete=false, "washi") +api.mapbox.com_d73a2016dbd70a152bffadf02f4f04d7e4d6a0b7 (T4 "princ" locale it) +api.mapbox.com_fee388586d54f2150af453c6e033e452d85e46a8 (T2 "1600 Pennsylvania Avenue NW, Washington, DC 20500") +``` + +Names are `api.mapbox.com_` of the exact request URL with the token replaced by the +literal string `[apikey]` (how `CachedResponseClient` computes the key in CI). If any file +is missing, re-record with (requires `$MAPBOX_GEOCODING_KEY` in the environment): + +```bash +php -r ' +$key = getenv("MAPBOX_GEOCODING_KEY"); +$dir = "src/Provider/Mapbox/Tests/.cached_responses"; +$urls = [ + "https://api.mapbox.com/search/geocode/v6/forward?" . http_build_query(["q"=>"149 9th St, San Francisco, CA 94103","types"=>"address","limit"=>5,"access_token"=>$key]), + "https://api.mapbox.com/search/geocode/v6/forward?" . http_build_query(["q"=>"1600 Pennsylvania Avenue NW, Washington, DC 20500","types"=>"address","limit"=>5,"access_token"=>$key]), + "https://api.mapbox.com/search/geocode/v6/reverse?" . http_build_query(["longitude"=>2.388911,"latitude"=>48.8631507,"types"=>"address","limit"=>5,"access_token"=>$key]), + "https://api.mapbox.com/search/geocode/v6/forward?" . http_build_query(["q"=>"princ","bbox"=>"-11.36323261153737,35.82809688193029,34.33947713277206,59.05992036364424","types"=>"place,locality,neighborhood","language"=>"it","limit"=>1,"access_token"=>$key]), + "https://api.mapbox.com/search/geocode/v6/forward?" . http_build_query(["q"=>"washi","bbox"=>"-124.83609163,45.54372254,-116.91742984,49.00243912","types"=>"region,neighborhood,street,place,locality","autocomplete"=>"true","limit"=>5,"access_token"=>$key]), + "https://api.mapbox.com/search/geocode/v6/forward?" . http_build_query(["q"=>"washi","bbox"=>"-124.83609163,45.54372254,-116.91742984,49.00243912","types"=>"region,neighborhood,street,place,locality","autocomplete"=>"false","limit"=>5,"access_token"=>$key]), + "https://api.mapbox.com/search/geocode/v6/forward?" . http_build_query(["address_number"=>"2595","street"=>"Lucky John Dr","place"=>"Park City","region"=>"UT","postcode"=>"84060","types"=>"address","autocomplete"=>"false","limit"=>5,"access_token"=>$key]), +]; +foreach ($urls as $u) { + $cached = str_replace($key, "[apikey]", $u); + $ch = curl_init($u); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]); + $body = curl_exec($ch); + if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) !== 200) { fwrite(STDERR, "FAILED $u\n"); exit(1); } + file_put_contents("$dir/api.mapbox.com_" . sha1($cached), serialize((string) $body)); + echo sha1($cached), PHP_EOL; +} +' +``` + +Important: the URL parameter **order** above must stay identical to the provider +implementation in Task 6 (it determines the cache key). + +**Step 0.3: Commit design docs** + +```bash +git add docs/plans/2026-08-28-mapbox-v6-design.md docs/plans/2026-08-28-mapbox-v6.md +git commit -m "docs: add design and implementation plan for Mapbox Geocoding v6" +``` + +--- + +### Task 1: `MapboxAddress::getMatchCode()` / `withMatchCode()` + +**Files:** +- Test: `src/Provider/Mapbox/Tests/MapboxAddressTest.php` (create) +- Modify: `src/Provider/Mapbox/Model/MapboxAddress.php` + +**Step 1.1: Write the failing test** (create the file) + +```php +build(MapboxAddress::class); + + $this->assertNull($address->getMatchCode()); + + $matchCode = ['street' => 'matched', 'confidence' => 'exact']; + $address = $address->withMatchCode($matchCode); + + $this->assertSame($matchCode, $address->getMatchCode()); + $this->assertNull($address->withMatchCode($matchCode)->withMatchCode()->getMatchCode()); + } +} +``` + +**Step 1.2: Run test to verify it fails** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php` +Expected: FAIL with `Error: Call to undefined method Geocoder\Provider\Mapbox\Model\MapboxAddress::getMatchCode()` + +**Step 1.3: Write minimal implementation** + +Add to `MapboxAddress.php` (matching the file's untyped-property + docblock style): + +```php + /** + * @var array|null + */ + private $matchCode; + + /** + * @return array|null + */ + public function getMatchCode(): ?array + { + return $this->matchCode; + } + + /** + * @param array|null $matchCode + */ + public function withMatchCode(?array $matchCode = null): self + { + $new = clone $this; + $new->matchCode = $matchCode; + + return $new; + } +``` + +**Step 1.4: Run test to verify it passes** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php` +Expected: PASS + +**Step 1.5: Commit** + +```bash +git add src/Provider/Mapbox/Model/MapboxAddress.php src/Provider/Mapbox/Tests/MapboxAddressTest.php +git commit -m "feat(mapbox): add matchCode to MapboxAddress" +``` + +--- + +### Task 2: `MapboxAddress::getMatchConfidence()` / `withMatchConfidence()` + +**Files:** +- Test: `src/Provider/Mapbox/Tests/MapboxAddressTest.php` +- Modify: `src/Provider/Mapbox/Model/MapboxAddress.php` + +**Step 2.1: Write the failing test** (append to `MapboxAddressTest`) + +```php + public function testMatchConfidence(): void + { + $address = (new AddressBuilder('mapbox'))->build(MapboxAddress::class); + + $this->assertNull($address->getMatchConfidence()); + + $address = $address->withMatchConfidence('exact'); + + $this->assertSame('exact', $address->getMatchConfidence()); + $this->assertNull($address->withMatchConfidence('exact')->withMatchConfidence()->getMatchConfidence()); + } +``` + +**Step 2.2: Run test to verify it fails** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php --filter testMatchConfidence` +Expected: FAIL with `Error: Call to undefined method ...::getMatchConfidence()` + +**Step 2.3: Write minimal implementation** (in `MapboxAddress.php`) + +```php + /** + * @var string|null + */ + private $matchConfidence; + + public function getMatchConfidence(): ?string + { + return $this->matchConfidence; + } + + public function withMatchConfidence(?string $matchConfidence = null): self + { + $new = clone $this; + $new->matchConfidence = $matchConfidence; + + return $new; + } +``` + +**Step 2.4: Run test to verify it passes** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php` +Expected: PASS (2 tests) + +**Step 2.5: Commit** + +```bash +git add src/Provider/Mapbox/Model/MapboxAddress.php src/Provider/Mapbox/Tests/MapboxAddressTest.php +git commit -m "feat(mapbox): add matchConfidence to MapboxAddress" +``` + +--- + +### Task 3: `MapboxAddress::getAccuracy()` / `withAccuracy()` + +**Files:** +- Test: `src/Provider/Mapbox/Tests/MapboxAddressTest.php` +- Modify: `src/Provider/Mapbox/Model/MapboxAddress.php` + +**Step 3.1: Write the failing test** (append to `MapboxAddressTest`) + +```php + public function testAccuracy(): void + { + $address = (new AddressBuilder('mapbox'))->build(MapboxAddress::class); + + $this->assertNull($address->getAccuracy()); + + $address = $address->withAccuracy('rooftop'); + + $this->assertSame('rooftop', $address->getAccuracy()); + $this->assertNull($address->withAccuracy('rooftop')->withAccuracy()->getAccuracy()); + } +``` + +**Step 3.2: Run test to verify it fails** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php --filter testAccuracy` +Expected: FAIL with `Error: Call to undefined method ...::getAccuracy()` + +**Step 3.3: Write minimal implementation** (in `MapboxAddress.php`) + +```php + /** + * @var string|null + */ + private $accuracy; + + public function getAccuracy(): ?string + { + return $this->accuracy; + } + + public function withAccuracy(?string $accuracy = null): self + { + $new = clone $this; + $new->accuracy = $accuracy; + + return $new; + } +``` + +**Step 3.4: Run test to verify it passes** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php` +Expected: PASS (3 tests) + +**Step 3.5: Commit** + +```bash +git add src/Provider/Mapbox/Model/MapboxAddress.php src/Provider/Mapbox/Tests/MapboxAddressTest.php +git commit -m "feat(mapbox): add accuracy to MapboxAddress" +``` + +--- + +### Task 4: Replace the test file with the version-independent tests (green on old code) + +The old `MapboxTest.php` contains v5-specific integration tests (v5 fixtures, `TYPE_POI`, +`fuzzy_match`). They are removed here; their v6 replacements arrive in Task 5. + +**Files:** +- Modify (full rewrite): `src/Provider/Mapbox/Tests/MapboxTest.php` + +**Step 4.1: Replace `MapboxTest.php` with** + +```php +getMockedHttpClient(), 'access_token'); + $this->assertEquals('mapbox', $provider->getName()); + } + + public function testGeocodeWithLocalhostIPv4(): void + { + $this->expectException(\Geocoder\Exception\UnsupportedOperation::class); + $this->expectExceptionMessage('The Mapbox provider does not support IP addresses, only street addresses.'); + + $provider = new Mapbox($this->getMockedHttpClient(), 'access_token'); + $provider->geocodeQuery(GeocodeQuery::create('127.0.0.1')); + } + + public function testGeocodeWithLocalhostIPv6(): void + { + $this->expectException(\Geocoder\Exception\UnsupportedOperation::class); + $this->expectExceptionMessage('The Mapbox provider does not support IP addresses, only street addresses.'); + + $provider = new Mapbox($this->getMockedHttpClient(), 'access_token'); + $provider->geocodeQuery(GeocodeQuery::create('::1')); + } + + public function testGeocodeWithRealIp(): void + { + $this->expectException(\Geocoder\Exception\UnsupportedOperation::class); + $this->expectExceptionMessage('The Mapbox provider does not support IP addresses, only street addresses.'); + + $provider = new Mapbox($this->getHttpClient(), 'access_token'); + $provider->geocodeQuery(GeocodeQuery::create('74.200.247.59')); + } + + public function testGeocodeWithQuotaExceeded(): void + { + $this->expectException(\Geocoder\Exception\QuotaExceeded::class); + + $provider = new Mapbox($this->getMockedHttpClient('', 429), 'access_token'); + $provider->geocodeQuery(GeocodeQuery::create('10 avenue Gambetta, Paris, France')); + } + + public function testGeocodeWithInvalidApiKey(): void + { + // The v6 API answers 401 for an invalid token (v5 answered 403). + // Both status codes map to InvalidCredentials in AbstractHttpProvider. + $this->expectException(\Geocoder\Exception\InvalidCredentials::class); + + $provider = new Mapbox($this->getMockedHttpClient('', 401), 'api_key'); + $provider->geocodeQuery(GeocodeQuery::create('10 avenue Gambetta, Paris, France')); + } + + public function testReverseWithInvalidResponse(): void + { + $this->expectException(InvalidServerResponse::class); + + $provider = new Mapbox($this->getMockedHttpClient(), 'access_token'); + $provider->reverseQuery(ReverseQuery::fromCoordinates(1, 2)); + } +} +``` + +**Step 4.2: Run tests to verify they pass (old v5 implementation still in place)** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` +Expected: PASS (7 tests) — these behaviors are version-independent, so they pass on the old +code. (This commit deliberately leaves the suite without Mapbox integration tests; Task 5 +restores them as v6 tests.) + +**Step 4.3: Commit** + +```bash +git add src/Provider/Mapbox/Tests/MapboxTest.php +git commit -m "test(mapbox): prepare test suite for the v6 API" +``` + +--- + +### Task 5: Add the failing v6 tests (RED) + +**Files:** +- Modify: `src/Provider/Mapbox/Tests/MapboxTest.php` (imports + new methods) +- Modify: `src/Provider/Mapbox/Mapbox.php` (ONLY the new constants — Step 5.3, required so + the test file can load at all; the rest of the implementation arrives in Tasks 6–7) + +**Step 5.1: Extend the `use` block** (top of `MapboxTest.php`, final state) + +```php +use Geocoder\IntegrationTest\BaseTestCase; +use Geocoder\Exception\InvalidServerResponse; +use Geocoder\Model\AddressCollection; +use Geocoder\Model\Bounds; +use Geocoder\Provider\Mapbox\Mapbox; +use Geocoder\Provider\Mapbox\Model\MapboxAddress; +use Geocoder\Query\GeocodeQuery; +use Geocoder\Query\ReverseQuery; +use Psr\Http\Message\RequestInterface; +``` + +**Step 5.2: Add the URL-construction tests** (before the final `}` of the class) + +```php + private function assertRequestUrl(string $expectedUrl, callable $request): void + { + $uri = ''; + $client = $this->getMockedHttpClientCallback(function (RequestInterface $request) use (&$uri) { + $uri = (string) $request->getUri(); + }); + + try { + $request($client); + } catch (InvalidServerResponse $e) { + // the mocked client answers an empty body; we only assert the request + } + + $this->assertSame($expectedUrl, $uri); + } + + public function testForwardGeocodeUrl(): void + { + $this->assertRequestUrl( + 'https://api.mapbox.com/search/geocode/v6/forward' + .'?q=10+Downing+St%2C+London' + .'&types=address' + .'&limit=5' + .'&access_token=access_token', + static function ($client) { + (new Mapbox($client, 'access_token'))->geocodeQuery(GeocodeQuery::create('10 Downing St, London')); + } + ); + } + + public function testForwardGeocodeUrlWithAllOptions(): void + { + $this->assertRequestUrl( + 'https://api.mapbox.com/search/geocode/v6/forward' + .'?q=wahsington' + .'&bbox=-124.83609163%2C45.54372254%2C-116.91742984%2C49.00243912' + .'&types=address%2Cstreet' + .'&autocomplete=true' + .'&proximity=-120.09%2C47.60' + .'&worldview=us' + .'&country=US' + .'&language=en' + .'&limit=10' + .'&permanent=true' + .'&access_token=access_token', + static function ($client) { + $provider = new Mapbox($client, 'access_token', 'US', true); + $query = GeocodeQuery::create('wahsington') + ->withLocale('en') + ->withBounds(new Bounds(45.54372254, -124.83609163, 49.00243912, -116.91742984)) + ->withLimit(10) + ->withData('location_type', [Mapbox::TYPE_ADDRESS, Mapbox::TYPE_STREET]) + ->withData('autocomplete', true) + ->withData('proximity', '-120.09,47.60') + ->withData('worldview', 'us'); + $provider->geocodeQuery($query); + } + ); + } + + public function testForwardGeocodeUrlWithStructuredInput(): void + { + // Structured Input: the query text is NOT sent; fields use their v6 names. + // autocomplete defaults to false for structured input (Mapbox guidance). + $this->assertRequestUrl( + 'https://api.mapbox.com/search/geocode/v6/forward' + .'?address_number=2595' + .'&street=Lucky+John+Dr' + .'&place=Park+City' + .'®ion=UT' + .'&postcode=84060' + .'&types=address' + .'&autocomplete=false' + .'&country=US' + .'&limit=5' + .'&access_token=access_token', + static function ($client) { + $provider = new Mapbox($client, 'access_token'); + $query = GeocodeQuery::create('2595 Lucky John Dr, Park City, UT 84060') + ->withData('address_number', '2595') + ->withData('street', 'Lucky John Dr') + ->withData('place', 'Park City') + ->withData('region', 'UT') + ->withData('postcode', '84060') + ->withData('country', 'US'); + $provider->geocodeQuery($query); + } + ); + } + + public function testForwardGeocodeUrlWithStructuredInputAndExplicitAutocomplete(): void + { + // an explicit autocomplete value overrides the structured-input default + $this->assertRequestUrl( + 'https://api.mapbox.com/search/geocode/v6/forward' + .'?street=9th+Street' + .'&types=address' + .'&autocomplete=true' + .'&limit=5' + .'&access_token=access_token', + static function ($client) { + $provider = new Mapbox($client, 'access_token'); + $query = GeocodeQuery::create('9th Street') + ->withData('street', '9th Street') + ->withData('autocomplete', true); + $provider->geocodeQuery($query); + } + ); + } + + public function testReverseGeocodeUrl(): void + { + $this->assertRequestUrl( + 'https://api.mapbox.com/search/geocode/v6/reverse' + .'?longitude=2.388911' + .'&latitude=48.8631507' + .'&types=address' + .'&limit=5' + .'&access_token=access_token', + static function ($client) { + (new Mapbox($client, 'access_token'))->reverseQuery(ReverseQuery::fromCoordinates(48.8631507, 2.388911)); + } + ); + } + + public function testReverseGeocodeUrlWithLocationTypeAndCountry(): void + { + $this->assertRequestUrl( + 'https://api.mapbox.com/search/geocode/v6/reverse' + .'?longitude=2.388911' + .'&latitude=48.8631507' + .'&types=address%2Cpostcode' + .'&country=FR' + .'&limit=2' + .'&access_token=access_token', + static function ($client) { + $provider = new Mapbox($client, 'access_token'); + $query = ReverseQuery::fromCoordinates(48.8631507, 2.388911) + ->withData('location_type', [Mapbox::TYPE_ADDRESS, Mapbox::TYPE_POSTCODE]) + ->withData('country', 'FR') + ->withLimit(2); + $provider->reverseQuery($query); + } + ); + } +``` + +(The remaining test methods — response parsing + integration — are in the section below; +append them before the final `}` as well.) + +**Step 5.3: Add the new constants so the test file can load** + +Minimal production code required for the suite to run (tests reference +`Mapbox::TYPE_STREET` / `Mapbox::TYPE_POSTCODE`). Append to the constants block of +`src/Provider/Mapbox/Mapbox.php`: + +```php + /** + * @var string + */ + public const TYPE_STREET = 'street'; + + /** + * Japanese addresses only. The API accepts this type only for Japan-context requests + * (e.g. with country=jp and language=ja), so it is intentionally not part of TYPES. + * + * @var string + */ + public const TYPE_BLOCK = 'block'; + + /** + * Sub-unit, suite, or lot within a parent address (US only). The API accepts this type + * only for US-context requests, so it is intentionally not part of TYPES. + * + * @var string + */ + public const TYPE_SECONDARY_ADDRESS = 'secondary_address'; + + /** + * Query data keys that trigger the v6 Structured Input mode (the query text is not sent). + * + * @var string[] + */ + public const STRUCTURED_INPUT_FIELDS = [ + 'address_line1', + 'address_number', + 'street', + 'block', + 'place', + 'region', + 'postcode', + 'locality', + 'neighborhood', + ]; +``` + +**Step 5.4: Run the tests to verify they FAIL for the right reasons (RED)** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` +Expected: +- the 7 version-independent tests PASS; +- the 6 URL-construction tests FAIL (old v5 URLs / 4th constructor arg still a string); +- the parsing tests FAIL (old v5 parser cannot handle the v6 `context` object); +- the 7 integration tests FAIL (provider builds v5 URLs → cache miss → live call with the + placeholder key → 401 → `InvalidCredentials`). + +**Step 5.5: Commit the failing tests** + +```bash +git add src/Provider/Mapbox/Tests/MapboxTest.php src/Provider/Mapbox/Mapbox.php +git commit -m "test(mapbox): add failing tests for the v6 API" +``` +**Step 5.2 (continued): append the response-parsing and integration tests** (before the +final `}`, after the URL-construction tests) + +## Response parsing (mocked v6 JSON) + +```php + public function testParseForwardAddressFeature(): void + { + $json = <<<'JSON' + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "dXJuOm1ieGFkcjo3Njg3YjZmNy01YmZkLTQzMjItOGMzOS02OGE1NDhmZWYwM2U", + "geometry": { "type": "Point", "coordinates": [-122.413709, 37.7757] }, + "properties": { + "mapbox_id": "dXJuOm1ieGFkcjo3Njg3YjZmNy01YmZkLTQzMjItOGMzOS02OGE1NDhmZWYwM2U", + "feature_type": "address", + "name": "149 9th Street", + "full_address": "149 9th Street, San Francisco, California 94103, United States", + "coordinates": { "longitude": -122.413709, "latitude": 37.7757, "accuracy": "rooftop" }, + "match_code": { "address_number": "matched", "street": "matched", "confidence": "exact" }, + "context": { + "address": { "address_number": "149", "street_name": "9th Street", "name": "149 9th Street" }, + "street": { "name": "9th Street" }, + "neighborhood": { "name": "South of Market" }, + "postcode": { "name": "94103" }, + "place": { "name": "San Francisco" }, + "region": { "name": "California", "region_code": "CA", "region_code_full": "US-CA" }, + "country": { "name": "United States", "country_code": "US", "country_code_alpha_3": "USA" } + } + } + } + ] + } + JSON; + + $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); + $results = $provider->geocodeQuery(GeocodeQuery::create('149 9th St, San Francisco, CA 94103')); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(1, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(MapboxAddress::class, $result); + $this->assertEqualsWithDelta(37.7757, $result->getCoordinates()->getLatitude(), 0.00001); + $this->assertEqualsWithDelta(-122.413709, $result->getCoordinates()->getLongitude(), 0.00001); + $this->assertEquals('dXJuOm1ieGFkcjo3Njg3YjZmNy01YmZkLTQzMjItOGMzOS02OGE1NDhmZWYwM2U', $result->getId()); + $this->assertEquals('9th Street', $result->getStreetName()); + $this->assertEquals('149', $result->getStreetNumber()); + $this->assertEquals('94103', $result->getPostalCode()); + $this->assertEquals('San Francisco', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('San Francisco', $result->getAdminLevels()->get(1)->getName()); + $this->assertEquals('California', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('CA', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('United States', $result->getCountry()->getName()); + $this->assertEquals('US', $result->getCountry()->getCode()); + $this->assertEquals('149 9th Street, San Francisco, California 94103, United States', $result->getFormattedAddress()); + $this->assertEquals('South of Market', $result->getNeighborhood()); + $this->assertEquals(['address'], $result->getResultType()); + $this->assertEquals('exact', $result->getMatchConfidence()); + $this->assertEquals('matched', $result->getMatchCode()['street']); + $this->assertEquals('rooftop', $result->getAccuracy()); + $this->assertNull($result->getTimezone()); + } + + public function testParseForwardPlaceFeature(): void + { + // place feature: no context.address; name is the place name; region code may be 3 letters + $json = <<<'JSON' + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "dXJuOm1ieHBsYzpoMmhQ", + "geometry": { "type": "Point", "coordinates": [-0.83069, 51.724422] }, + "properties": { + "mapbox_id": "dXJuOm1ieHBsYzpoMmhQ", + "feature_type": "place", + "name": "Princes Risborough", + "full_address": "Princes Risborough, Buckinghamshire, Inghilterra, Regno Unito", + "coordinates": { "longitude": -0.83069, "latitude": 51.724422 }, + "context": { + "place": { "name": "Princes Risborough" }, + "region": { "name": "Inghilterra", "region_code": "ENG" }, + "country": { "name": "Regno Unito", "country_code": "GB" } + } + } + } + ] + } + JSON; + + $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); + $results = $provider->geocodeQuery(GeocodeQuery::create('princ')); + + $this->assertCount(1, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertEquals('dXJuOm1ieHBsYzpoMmhQ', $result->getId()); + $this->assertEquals('Princes Risborough', $result->getStreetName()); + $this->assertNull($result->getStreetNumber()); + $this->assertNull($result->getPostalCode()); + $this->assertEquals('Princes Risborough', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('Princes Risborough', $result->getAdminLevels()->get(1)->getName()); + $this->assertEquals('Inghilterra', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('ENG', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('Regno Unito', $result->getCountry()->getName()); + $this->assertEquals('GB', $result->getCountry()->getCode()); + $this->assertEquals('Princes Risborough, Buckinghamshire, Inghilterra, Regno Unito', $result->getFormattedAddress()); + $this->assertNull($result->getNeighborhood()); + $this->assertEquals(['place'], $result->getResultType()); + $this->assertNull($result->getMatchConfidence()); + $this->assertNull($result->getMatchCode()); + $this->assertNull($result->getAccuracy()); + } + + public function testParseForwardStreetFeature(): void + { + // v6 street feature: street name comes from context.street + $json = <<<'JSON' + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "address.7607992201284362", + "geometry": { "type": "Point", "coordinates": [-117.039767, 46.930253] }, + "properties": { + "mapbox_id": "address.7607992201284362", + "feature_type": "street", + "name": "Washington Highway 6", + "full_address": "Washington Highway 6, Palouse, Washington 99161, United States", + "coordinates": { "longitude": -117.039767, "latitude": 46.930253 }, + "context": { + "street": { "name": "Washington Highway 6" }, + "postcode": { "name": "99161" }, + "place": { "name": "Palouse" }, + "region": { "name": "Washington", "region_code": "WA" }, + "country": { "name": "United States", "country_code": "US" } + } + } + } + ] + } + JSON; + + $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); + $results = $provider->geocodeQuery(GeocodeQuery::create('Washington Highway 6')); + + $this->assertCount(1, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertEquals('address.7607992201284362', $result->getId()); + $this->assertEquals('Washington Highway 6', $result->getStreetName()); + $this->assertNull($result->getStreetNumber()); + $this->assertEquals('99161', $result->getPostalCode()); + $this->assertEquals('Palouse', $result->getLocality()); + $this->assertEquals('Washington', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('WA', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals(['street'], $result->getResultType()); + } + + public function testParseFeatureWithoutContext(): void + { + // features without a context object must still parse (name + coordinates) + $json = <<<'JSON' + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "abc123", + "geometry": { "type": "Point", "coordinates": [2.35, 48.85] }, + "properties": { + "mapbox_id": "abc123", + "feature_type": "place", + "name": "Some Place", + "coordinates": { "longitude": 2.35, "latitude": 48.85 } + } + } + ] + } + JSON; + + $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); + $results = $provider->geocodeQuery(GeocodeQuery::create('Some Place')); + + $this->assertCount(1, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertEquals('Some Place', $result->getStreetName()); + $this->assertEquals('abc123', $result->getId()); + $this->assertEqualsWithDelta(48.85, $result->getCoordinates()->getLatitude(), 0.00001); + $this->assertEqualsWithDelta(2.35, $result->getCoordinates()->getLongitude(), 0.00001); + } + + public function testParseEmptyFeatures(): void + { + $json = '{"type":"FeatureCollection","features":[]}'; + + $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); + $results = $provider->geocodeQuery(GeocodeQuery::create('jsajhgsdkfjhsfkjhaldkadjaslgldasd')); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertEmpty($results); + } + + public function testParseInvalidJson(): void + { + $this->expectException(InvalidServerResponse::class); + + $provider = new Mapbox($this->getMockedHttpClient('{invalid json'), 'access_token'); + $provider->geocodeQuery(GeocodeQuery::create('10 Downing St, London')); + } +``` + +## Integration tests (replayed from `.cached_responses`) + +All seven use the standard guard + client construction: + +```php + if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { + $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); + } + + $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); +``` + +```php + public function testGeocodeWithRealAddress(): void + { + // guard + $provider as above + $results = $provider->geocodeQuery(GeocodeQuery::create('149 9th St, San Francisco, CA 94103')); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(5, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(MapboxAddress::class, $result); + $this->assertEqualsWithDelta(37.7757, $result->getCoordinates()->getLatitude(), 0.001); + $this->assertEqualsWithDelta(-122.413709, $result->getCoordinates()->getLongitude(), 0.001); + $this->assertEquals('9th Street', $result->getStreetName()); + $this->assertEquals('149', $result->getStreetNumber()); + $this->assertEquals('94103', $result->getPostalCode()); + $this->assertEquals('San Francisco', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('California', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('CA', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('United States', $result->getCountry()->getName()); + $this->assertEquals('US', $result->getCountry()->getCode()); + $this->assertEquals('149 9th Street, San Francisco, California 94103, United States', $result->getFormattedAddress()); + $this->assertEquals('South of Market', $result->getNeighborhood()); + $this->assertEquals(['address'], $result->getResultType()); + $this->assertEquals('exact', $result->getMatchConfidence()); + $this->assertEquals('rooftop', $result->getAccuracy()); + + // not provided + $this->assertNull($result->getTimezone()); + } + + public function testGeocodeWithRealAddressInDc(): void + { + // guard + $provider as above + $results = $provider->geocodeQuery(GeocodeQuery::create('1600 Pennsylvania Avenue NW, Washington, DC 20500')); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(2, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(MapboxAddress::class, $result); + $this->assertEqualsWithDelta(38.897684, $result->getCoordinates()->getLatitude(), 0.001); + $this->assertEqualsWithDelta(-77.036574, $result->getCoordinates()->getLongitude(), 0.001); + $this->assertEquals('Pennsylvania Avenue Northwest', $result->getStreetName()); + $this->assertEquals('1600', $result->getStreetNumber()); + $this->assertEquals('20500', $result->getPostalCode()); + $this->assertEquals('Washington', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('District of Columbia', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('DC', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('United States', $result->getCountry()->getName()); + $this->assertEquals('US', $result->getCountry()->getCode()); + $this->assertEquals('1600 Pennsylvania Avenue Northwest, Washington, District of Columbia 20500, United States', $result->getFormattedAddress()); + $this->assertEquals('exact', $result->getMatchConfidence()); + $this->assertEquals('rooftop', $result->getAccuracy()); + } + + public function testReverseWithRealCoordinates(): void + { + // guard + $provider as above + $results = $provider->reverseQuery(ReverseQuery::fromCoordinates(48.8631507, 2.388911)); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(4, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(MapboxAddress::class, $result); + $this->assertEquals('12', $result->getStreetNumber()); + $this->assertEquals('Avenue Gambetta', $result->getStreetName()); + $this->assertEquals('75020', $result->getPostalCode()); + $this->assertEquals('Paris', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('Paris', $result->getAdminLevels()->get(1)->getName()); + $this->assertEquals('Île-de-France', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('IDF', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('France', $result->getCountry()->getName()); + $this->assertEquals('FR', $result->getCountry()->getCode()); + $this->assertEquals('12 Avenue Gambetta, 75020 Paris, France', $result->getFormattedAddress()); + $this->assertEquals('rooftop', $result->getAccuracy()); + + // not provided by reverse geocoding + $this->assertNull($result->getMatchConfidence()); + $this->assertNull($result->getNeighborhood()); + } + + public function testGeocodePlaceWithLocale(): void + { + // guard + $provider as above + $query = GeocodeQuery::create('princ'); + $query = $query->withLocale('it'); + $query = $query->withBounds(new Bounds( + 35.82809688193029, + -11.36323261153737, + 59.05992036364424, + 34.33947713277206 + )); + $query = $query->withLimit(1); + $query = $query->withData('location_type', [ + Mapbox::TYPE_PLACE, + Mapbox::TYPE_LOCALITY, + Mapbox::TYPE_NEIGHBORHOOD, + ]); + + $results = $provider->geocodeQuery($query); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(1, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(MapboxAddress::class, $result); + $this->assertEqualsWithDelta(51.724422, $result->getCoordinates()->getLatitude(), 0.001); + $this->assertEqualsWithDelta(-0.83069, $result->getCoordinates()->getLongitude(), 0.001); + $this->assertEquals('Princes Risborough', $result->getStreetName()); + $this->assertEquals(['place'], $result->getResultType()); + $this->assertEquals('Princes Risborough', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('Princes Risborough', $result->getAdminLevels()->get(1)->getName()); + $this->assertEquals('Inghilterra', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('ENG', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('Regno Unito', $result->getCountry()->getName()); + $this->assertEquals('GB', $result->getCountry()->getCode()); + + // not provided + $this->assertNull($result->getPostalCode()); + $this->assertNull($result->getStreetNumber()); + $this->assertNull($result->getMatchConfidence()); + } + + public function testGeocodeWithAutocompleteEnabled(): void + { + // guard + $provider as above + $query = GeocodeQuery::create('washi'); + $query = $query->withData('autocomplete', true); + $query = $query->withBounds(new Bounds( + 45.54372254, + -124.83609163, + 49.00243912, + -116.91742984 + )); + $query = $query->withData('location_type', [ + Mapbox::TYPE_REGION, + Mapbox::TYPE_NEIGHBORHOOD, + Mapbox::TYPE_STREET, + Mapbox::TYPE_PLACE, + Mapbox::TYPE_LOCALITY, + ]); + + $results = $provider->geocodeQuery($query); + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(5, $results); + + /** @var MapboxAddress $first */ + $first = $results->first(); + $this->assertEquals(['region'], $first->getResultType()); + $this->assertEquals('Washington', $first->getStreetName()); + $this->assertEquals('Washington', $first->getAdminLevels()->get(2)->getName()); + $this->assertEquals('WA', $first->getAdminLevels()->get(2)->getCode()); + } + + public function testGeocodeWithAutocompleteDisabled(): void + { + // guard + $provider as above + $query = GeocodeQuery::create('washi'); + $query = $query->withData('autocomplete', false); + $query = $query->withBounds(new Bounds( + 45.54372254, + -124.83609163, + 49.00243912, + -116.91742984 + )); + $query = $query->withData('location_type', [ + Mapbox::TYPE_REGION, + Mapbox::TYPE_NEIGHBORHOOD, + Mapbox::TYPE_STREET, + Mapbox::TYPE_PLACE, + Mapbox::TYPE_LOCALITY, + ]); + + $results = $provider->geocodeQuery($query); + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(1, $results); + + /** @var MapboxAddress $first */ + $first = $results->first(); + $this->assertEquals(['street'], $first->getResultType()); + $this->assertEquals('Yashi Road', $first->getStreetName()); + } + + public function testGeocodeWithStructuredInput(): void + { + // guard + $provider as above + $query = GeocodeQuery::create('2595 Lucky John Dr, Park City, UT 84060'); + $query = $query->withData('address_number', '2595'); + $query = $query->withData('street', 'Lucky John Dr'); + $query = $query->withData('place', 'Park City'); + $query = $query->withData('region', 'UT'); + $query = $query->withData('postcode', '84060'); + + $results = $provider->geocodeQuery($query); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(1, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(MapboxAddress::class, $result); + $this->assertEqualsWithDelta(40.6716, $result->getCoordinates()->getLatitude(), 0.001); + $this->assertEqualsWithDelta(-111.507008, $result->getCoordinates()->getLongitude(), 0.001); + $this->assertEquals('Lucky John Drive', $result->getStreetName()); + $this->assertEquals('2595', $result->getStreetNumber()); + $this->assertEquals('84060', $result->getPostalCode()); + $this->assertEquals('Park City', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('Park City', $result->getAdminLevels()->get(1)->getName()); + $this->assertEquals('Utah', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('UT', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('United States', $result->getCountry()->getName()); + $this->assertEquals('US', $result->getCountry()->getCode()); + $this->assertEquals('2595 Lucky John Drive, Park City, Utah 84060, United States', $result->getFormattedAddress()); + $this->assertEquals('Park Meadows', $result->getNeighborhood()); + $this->assertEquals('exact', $result->getMatchConfidence()); + $this->assertEquals('rooftop', $result->getAccuracy()); + } +``` + +**Step 5.3: Add the new constants so the test file can load** + +This is the minimal production code required for the suite to run at all (tests reference +`Mapbox::TYPE_STREET` etc.). Insert into `src/Provider/Mapbox/Mapbox.php` after +`TYPE_ADDRESS`: + +```php + /** + * @var string + */ + public const TYPE_STREET = 'street'; + + /** + * Japanese addresses only. The API accepts this type only for Japan-context requests + * (e.g. country=jp + language=ja), so it is intentionally not part of TYPES. + * + * @var string + */ + public const TYPE_BLOCK = 'block'; + + /** + * Sub-unit, suite or lot within a parent address (US only). The API accepts this type + * only for US-context requests, so it is intentionally not part of TYPES. + * + * @var string + */ + public const TYPE_SECONDARY_ADDRESS = 'secondary_address'; +``` + +**Step 5.4: Run the suite to verify the NEW tests fail (RED)** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` +Expected: +- the 7 Task-4 tests PASS (still); +- the 6 URL-construction tests FAIL with `Failed asserting that 'https://api.mapbox.com/geocoding/v5/...' === 'https://api.mapbox.com/search/geocode/v6/...'` (provider still builds v5 URLs); +- the 6 parsing tests FAIL (old parser chokes on the v6 `properties`/`context` shape — warnings/assertion failures, not the expected model values); +- the 7 integration tests FAIL with `Geocoder\Exception\InvalidCredentials` (provider builds v5 URLs → cache miss → live call with the placeholder key → 401). + +If a test fails for an unexpected reason (typo in a fixture, wrong assertion), fix the TEST, +re-run, and re-verify the RED state. Do not proceed while the RED state is not as expected. + +**Step 5.5: Commit** + +```bash +git add src/Provider/Mapbox/Tests/MapboxTest.php src/Provider/Mapbox/Mapbox.php +git commit -m "test(mapbox): add failing tests for the v6 API" +``` + +--- + +### Task 6: Implement v6 request building (GREEN for URL tests) + +**Files:** +- Modify: `src/Provider/Mapbox/Mapbox.php` + +**Step 6.1: Replace the endpoint constants** + +```php + /** + * @var string + */ + public const FORWARD_ENDPOINT_URL = 'https://api.mapbox.com/search/geocode/v6/forward'; + + /** + * @var string + */ + public const REVERSE_ENDPOINT_URL = 'https://api.mapbox.com/search/geocode/v6/reverse'; +``` + +(Delete `GEOCODE_ENDPOINT_URL_SSL` and `REVERSE_ENDPOINT_URL_SSL`.) + +**Step 6.2: Replace the mode constants and update TYPES** + +Delete `GEOCODING_MODE_PLACES`, `GEOCODING_MODE_PLACES_PERMANENT`, `GEOCODING_MODES`, +`TYPE_POI`, `TYPE_POI_LANDMARK`. Final types block: + +```php + /** + * @var string + */ + public const TYPE_COUNTRY = 'country'; + // ... (existing TYPE_REGION, TYPE_POSTCODE, TYPE_DISTRICT, TYPE_PLACE, + // TYPE_LOCALITY, TYPE_NEIGHBORHOOD, TYPE_ADDRESS unchanged) + + /** + * @var string[] + */ + public const TYPES = [ + self::TYPE_COUNTRY, + self::TYPE_REGION, + self::TYPE_POSTCODE, + self::TYPE_DISTRICT, + self::TYPE_PLACE, + self::TYPE_LOCALITY, + self::TYPE_NEIGHBORHOOD, + self::TYPE_STREET, + self::TYPE_ADDRESS, + ]; +``` + +Plus the `TYPE_STREET` / `TYPE_BLOCK` / `TYPE_SECONDARY_ADDRESS` constants from Step 5.3 and: + +```php + /** + * Query data keys that trigger the v6 Structured Input mode (the query text is not sent). + * + * @var string[] + */ + public const STRUCTURED_INPUT_FIELDS = [ + 'address_line1', + 'address_number', + 'street', + 'block', + 'place', + 'region', + 'postcode', + 'locality', + 'neighborhood', + ]; +``` + +**Step 6.3: Replace the constructor and the two query methods** + +```php + /** + * @var bool + */ + private $permanent; + + /** + * @param ClientInterface $client An HTTP adapter + * @param string $accessToken Your Mapbox access token + * @param string|null $country Restrict results to one or more ISO 3166 alpha-2 countries (comma separated) + * @param bool $permanent Store results permanently (v6 `permanent` parameter) + */ + public function __construct( + ClientInterface $client, + string $accessToken, + ?string $country = null, + bool $permanent = false, + ) { + parent::__construct($client); + + $this->accessToken = $accessToken; + $this->country = $country; + $this->permanent = $permanent; + } + + public function geocodeQuery(GeocodeQuery $query): Collection + { + // The Mapbox API does not geocode raw IP addresses + if (filter_var($query->getText(), FILTER_VALIDATE_IP)) { + throw new UnsupportedOperation('The Mapbox provider does not support IP addresses, only street addresses.'); + } + + $parameters = []; + + // v6 Structured Input: typed fields replace the `q` search text + $structured = $this->structuredInputFields($query); + if ([] !== $structured) { + foreach (self::STRUCTURED_INPUT_FIELDS as $field) { + if (isset($structured[$field])) { + $parameters[$field] = $structured[$field]; + } + } + } else { + $parameters['q'] = $query->getText(); + } + + if ($query->getBounds()) { + // Format is "minLon,minLat,maxLon,maxLat" + $parameters['bbox'] = sprintf( + '%s,%s,%s,%s', + $query->getBounds()->getWest(), + $query->getBounds()->getSouth(), + $query->getBounds()->getEast(), + $query->getBounds()->getNorth() + ); + } + + $parameters['types'] = $this->locationTypes($query); + + if (null !== $autocomplete = $query->getData('autocomplete')) { + $parameters['autocomplete'] = $autocomplete ? 'true' : 'false'; + } elseif ([] !== $structured) { + // Mapbox recommends autocomplete=false for structured input + $parameters['autocomplete'] = 'false'; + } + + if (null !== $proximity = $query->getData('proximity')) { + $parameters['proximity'] = $proximity; + } + + if (null !== $worldview = $query->getData('worldview')) { + $parameters['worldview'] = $worldview; + } + + return $this->fetchUrl( + self::FORWARD_ENDPOINT_URL, + $query->getLimit(), + $query->getLocale(), + $query->getData('country', $this->country), + $parameters + ); + } + + public function reverseQuery(ReverseQuery $query): Collection + { + $coordinate = $query->getCoordinates(); + $parameters = [ + 'longitude' => $coordinate->getLongitude(), + 'latitude' => $coordinate->getLatitude(), + ]; + + $parameters['types'] = $this->locationTypes($query); + + return $this->fetchUrl( + self::REVERSE_ENDPOINT_URL, + $query->getLimit(), + $query->getLocale(), + $query->getData('country', $this->country), + $parameters + ); + } + + /** + * @return array + */ + 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; + } + + /** + * @param GeocodeQuery|ReverseQuery $query + */ + private function locationTypes($query): string + { + if (null !== $locationType = $query->getData('location_type')) { + return is_array($locationType) ? implode(',', $locationType) : $locationType; + } + + return self::DEFAULT_TYPE; + } +``` + +**Step 6.4: Replace `buildQuery`** + +```php + /** + * @param array $parameters + */ + private function buildQuery(string $url, int $limit, ?string $locale, ?string $country, array $parameters): string + { + if (null !== $country) { + $parameters['country'] = $country; + } + + if (null !== $locale) { + $parameters['language'] = $locale; + } + + $parameters['limit'] = $limit; + + if ($this->permanent) { + $parameters['permanent'] = 'true'; + } + + $parameters['access_token'] = $this->accessToken; + + return $url.'?'.http_build_query($parameters); + } +``` + +(Parameter insertion order is part of the observable URL — see design doc; the +integration fixtures in Task 0 depend on it.) + +**Step 6.5: Adapt `fetchUrl`'s signature** (keep v5 body for now; parsing changes in Task 7) + +```php + /** + * @param array $parameters + */ + private function fetchUrl(string $url, int $limit, ?string $locale, ?string $country, array $parameters): AddressCollection + { + $url = $this->buildQuery($url, $limit, $locale, $country, $parameters); + $content = $this->getUrlContents($url); + // ... existing v5 body stays until Task 7 + } +``` + +**Step 6.6: Run the URL tests to verify they pass (GREEN)** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php --filter "Url"` +Expected: PASS (6 URL tests). Parsing + integration tests still fail (expected). + +**Step 6.7: Commit** + +```bash +git add src/Provider/Mapbox/Mapbox.php +git commit -m "feat(mapbox): build requests for the Geocoding API v6" +``` + +--- + +### Task 7: Implement v6 response parsing (GREEN for parsing tests) + +**Files:** +- Modify: `src/Provider/Mapbox/Mapbox.php` + +**Step 7.1: Replace the parsing section of `fetchUrl` and the helpers** + +`validateResponse(string $url, string $content): array` and +`parseCoordinates(AddressBuilder $builder, array $result): void` stay unchanged from v5 +(`geometry.coordinates` = [lon, lat] → `setCoordinates(lat, lon)`; feature `bbox` +[minLon, minLat, maxLon, maxLat] → `setBounds(south, west, north, east)` when present — +documented for non-address features but not currently returned; map defensively). + +Replace `updateAddressComponent` and the feature loop with: + +```php + private function fetchUrl(string $url, int $limit, ?string $locale, ?string $country, array $parameters): AddressCollection + { + $url = $this->buildQuery($url, $limit, $locale, $country, $parameters); + $content = $this->getUrlContents($url); + $json = $this->validateResponse($url, $content); + + // no result + if (!isset($json['features']) || !count($json['features'])) { + return new AddressCollection([]); + } + + $results = []; + foreach ($json['features'] as $result) { + $results[] = $this->parseFeature($result); + + if (count($results) >= $limit) { + break; + } + } + + return new AddressCollection($results); + } + + /** + * @param array $result + */ + private function parseFeature(array $result): MapboxAddress + { + $properties = $result['properties'] ?? []; + $context = $properties['context'] ?? []; + + $builder = new AddressBuilder($this->getName()); + $this->parseCoordinates($builder, $result); + + // update address components (context is an object keyed by feature type in v6) + foreach ($context as $type => $component) { + if (is_array($component)) { + $this->updateAddressComponent($builder, (string) $type, $component); + } + } + + $address = $builder->build(MapboxAddress::class); + + if (isset($properties['mapbox_id'])) { + $address = $address->withId((string) $properties['mapbox_id']); + } elseif (isset($result['id'])) { + $address = $address->withId((string) $result['id']); + } + + // street name without the house number (v5 semantics) + $streetName = $context['address']['street_name'] ?? $context['street']['name'] ?? $properties['name'] ?? null; + if (null !== $streetName) { + $address = $address->withStreetName((string) $streetName); + } + + $streetNumber = $context['address']['address_number'] ?? null; + if (null !== $streetNumber) { + $address = $address->withStreetNumber((string) $streetNumber); + } + + if (isset($properties['feature_type'])) { + $address = $address->withResultType([(string) $properties['feature_type']]); + } + + if (isset($properties['full_address'])) { + $address = $address->withFormattedAddress((string) $properties['full_address']); + } + + $neighborhood = $context['neighborhood']['name'] ?? null; + if (null !== $neighborhood && '' !== $neighborhood) { + $address = $address->withNeighborhood((string) $neighborhood); + } + + if (isset($properties['match_code']) && is_array($properties['match_code'])) { + $address = $address->withMatchCode($properties['match_code']); + $confidence = $properties['match_code']['confidence'] ?? null; + if (null !== $confidence) { + $address = $address->withMatchConfidence((string) $confidence); + } + } + + if (isset($properties['coordinates']['accuracy'])) { + $address = $address->withAccuracy((string) $properties['coordinates']['accuracy']); + } + + return $address; + } + + /** + * Update current resultSet with given key/value. + * + * @param array $component + */ + private function updateAddressComponent(AddressBuilder $builder, string $type, array $component): void + { + if (!isset($component['name']) || '' === $component['name']) { + return; + } + + switch ($type) { + case 'postcode': + $builder->setPostalCode($component['name']); + + break; + + case 'locality': + $builder->setLocality($component['name']); + + break; + + case 'country': + $builder->setCountry($component['name']); + if (isset($component['country_code'])) { + $builder->setCountryCode(strtoupper((string) $component['country_code'])); + } + + break; + + case 'place': + $builder->addAdminLevel(1, $component['name']); + $builder->setLocality($component['name']); + + break; + + case 'region': + $code = isset($component['region_code']) ? (string) $component['region_code'] : null; + $builder->addAdminLevel(2, $component['name'], $code); + + break; + + default: + // address, street, district, neighborhood: handled (or intentionally ignored) elsewhere + } + } +``` + +Notes: +- Context iteration order is the JSON key order; later entries win for `locality` + (`locality` then `place`), matching the v5 precedence. +- `district` is intentionally ignored (same as v5). +- Remove the now-unused `use Geocoder\Exception\InvalidArgument;` import if it became + unused (the mode-validation is gone). + +**Step 7.2: Run the parsing tests to verify they pass (GREEN)** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php --filter "Parse"` +Expected: PASS (6 parsing tests). + +**Step 7.3: Run the whole Mapbox suite** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` +Expected: unit tests all PASS; the 7 integration tests still FAIL (see Task 8). + +**Step 7.4: Commit** + +```bash +git add src/Provider/Mapbox/Mapbox.php +git commit -m "feat(mapbox): parse Geocoding API v6 responses" +``` + +--- + +### Task 8: Integration tests green (full Mapbox suite green) + +**Files:** none (verification only — if anything fails, fix the IMPLEMENTATION, re-run, +and keep going; do not edit test expectations to match broken output) + +**Step 8.1: Run the integration tests** + +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php --filter "testGeocodeWith|testReverseWith"` +Expected: all 7 integration tests PASS (fixtures replayed from `.cached_responses`). + +If a test fails with `InvalidCredentials`: the provider built a URL different from the +recorded one — diff the expected URL (Task 0 list / design doc parameter order) against the +actual. `CachedResponseClient` records a NEW file on cache miss, so a stray file appearing +in `.cached_responses` means a URL mismatch — delete the stray file and fix the provider. + +**Step 8.2: Run the full Mapbox suite + model tests** + +Run: +```bash +vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php +vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php +``` +Expected: ALL PASS (20 + 3 tests), no warnings or deprecations in the output. + +**Step 8.3: Commit** (nothing to commit if no changes — skip this step in that case) + +--- + +### Task 9: Cleanup, CHANGELOG, docs, full verification + +**Files:** +- Delete: stale files in `src/Provider/Mapbox/Tests/.cached_responses/` (everything except the 7 Task-0 fixtures) +- Modify: `src/Provider/Mapbox/CHANGELOG.md` +- Modify: `src/Provider/Mapbox/Readme.md` +- Modify: `README.md` (main, line ~137) + +**Step 9.1: Remove stale cache files** (keep only the 7 fixtures from Task 0) + +```bash +php -r ' +$keep = [ + "5e53474a50aa75267953e6d90b2e3472021efbf2", + "fee388586d54f2150af453c6e033e452d85e46a8", + "7b8b47952b295102acd4cb1bc0a9536c26c7bc9b", + "d73a2016dbd70a152bffadf02f4f04d7e4d6a0b7", + "338a05e2ceeb9a75e55774746945065683127870", + "9e3e5555aca6117bf14dbd86c37ed23cbece335d", + "06551ee827f3b9635793737514ec6b9d368075c4", +]; +$dir = "src/Provider/Mapbox/Tests/.cached_responses"; +foreach (glob("$dir/api.mapbox.com_*") as $f) { + if (!in_array(substr(basename($f), 14), $keep, true)) { + unlink($f); + echo "removed ", basename($f), PHP_EOL; + } +} +' +``` +Expected: 10 files removed (7 old v5 fixtures — 3 of them already deleted in the working +tree — plus 3 v6 zero-result probe files). Re-run the Mapbox suite to confirm the cleanup +broke nothing: +Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` +Expected: ALL PASS. + +**Step 9.2: Provider CHANGELOG** — prepend to `src/Provider/Mapbox/CHANGELOG.md` (after the +header, before `## 1.5.0`): + +```markdown +## 2.0.0 + +### Added + +- Support for the Mapbox Geocoding API v6 (`/search/geocode/v6/forward` and `/search/geocode/v6/reverse`) +- v6 Structured Input: forward queries built from typed address fields via query data — `address_line1`, `address_number`, `street`, `block`, `place`, `region`, `postcode`, `locality`, `neighborhood`. When any of these is present, the query text is not sent. +- `Mapbox::TYPE_STREET` (new v6 type, added to `Mapbox::TYPES`) +- `Mapbox::TYPE_BLOCK` (Japan) and `Mapbox::TYPE_SECONDARY_ADDRESS` (US) constants (not part of `TYPES`: the API only accepts them in the corresponding regional context) +- `Mapbox::STRUCTURED_INPUT_FIELDS` constant +- Query data support: `autocomplete` (replaces `fuzzy_match`), `proximity` (`"lon,lat"` or `"ip"`), `worldview` +- New 4th constructor argument `bool $permanent` (v6 `permanent=true` result storage) +- `MapboxAddress::getMatchCode()` / `withMatchCode()` — v6 Smart Address Match object +- `MapboxAddress::getMatchConfidence()` / `withMatchConfidence()` — `exact`, `high`, `medium` or `low` +- `MapboxAddress::getAccuracy()` / `withAccuracy()` — `rooftop`, `parcel`, `point`, `interpolated`, `approximate` or `intersection` + +### Removed + +- Support for the Mapbox Geocoding API v5 (`/geocoding/v5/...`) — pin to `geocoder-php/mapbox-provider:^1.5` to stay on v5 +- `Mapbox::GEOCODE_ENDPOINT_URL_SSL` and `Mapbox::REVERSE_ENDPOINT_URL_SSL` constants (replaced by `Mapbox::FORWARD_ENDPOINT_URL` / `Mapbox::REVERSE_ENDPOINT_URL`) +- `Mapbox::GEOCODING_MODE_PLACES`, `Mapbox::GEOCODING_MODE_PLACES_PERMANENT` and `Mapbox::GEOCODING_MODES` constants (v6 has no geocoding modes; use the `permanent` constructor argument) +- `Mapbox::TYPE_POI` and `Mapbox::TYPE_POI_LANDMARK` constants (v6 does not geocode POIs; passing them as `location_type` now yields a 422 error from the API) +- `fuzzy_match` query data (v6 has no fuzzy/typo matching; use `autocomplete` for prefix-style matching) + +### Changed + +- The 4th constructor argument is now `bool $permanent = false` (was `string $geocodingMode`); passing a mode string changes behavior or errors +- `Mapbox::TYPES` no longer contains the POI types and now contains `street` +- Forward requests send the search text as the `q` query parameter instead of in the URL path +- Reverse requests send `longitude` / `latitude` query parameters (was `{lon},{lat}` in the path) +- `MapboxAddress::getId()` returns the v6 `mapbox_id` value (format changed, e.g. `dXJu...` instead of `address.123`) +- `MapboxAddress::getResultType()` values follow the v6 `feature_type` vocabulary (`street` added, `poi`/`poi.landmark` gone) +- Reverse geocoding with a `limit` greater than 1 requires a single `location_type` value (v6 API constraint: multiple types + limit returns a 422 error) +- Forward `limit` is capped at 10 by the v6 API (higher values are silently clamped) +- Invalid access tokens are now answered with HTTP 401 by the API (v5 used 403); both map to `Geocoder\Exception\InvalidCredentials` +- Geocoded data reflects the v6 dataset: coordinates, results and admin-level codes may differ from v5 +``` + +**Step 9.3: Provider README** — in `src/Provider/Mapbox/Readme.md`, after the `### Note` +paragraph, insert: + +```markdown +### Usage + +This provider targets the **Mapbox Geocoding API v6**. If you rely on the v5 API, pin +`geocoder-php/mapbox-provider` to `^1.5`. + +```php +$provider = new Mapbox($client, $accessToken); +// optional: country filter (ISO 3166 alpha-2) and permanent result storage +$provider = new Mapbox($client, $accessToken, 'US', true); + +// Forward geocoding +$provider->geocodeQuery(GeocodeQuery::create('149 9th St, San Francisco, CA 94103')); + +// Forward geocoding with options (query data) +$query = GeocodeQuery::create('washi') + ->withData('autocomplete', true) // prefix-style matching (on by default) + ->withData('location_type', Mapbox::TYPE_STREET) // or an array of Mapbox::TYPE_* constants + ->withData('proximity', '-120.09,47.60') // or 'ip' + ->withData('worldview', 'us') + ->withData('country', 'US'); + +// v6 Structured Input: typed fields replace the free-text query (the text is not sent) +$query = GeocodeQuery::create('2595 Lucky John Dr, Park City, UT 84060') + ->withData('address_number', '2595') + ->withData('street', 'Lucky John Dr') + ->withData('place', 'Park City') + ->withData('region', 'UT') + ->withData('postcode', '84060'); +// Any of address_line1, address_number, street, block, place, region, postcode, +// locality, neighborhood triggers Structured Input; autocomplete defaults to false. + +// Reverse geocoding +$provider->reverseQuery(ReverseQuery::fromCoordinates(48.8631507, 2.388911)); +``` + +Results are `Geocoder\Provider\Mapbox\Model\MapboxAddress` instances. Besides the regular +address data they expose `getId()`, `getStreetName()`, `getStreetNumber()`, +`getResultType()`, `getFormattedAddress()`, `getNeighborhood()`, +`getMatchCode()` / `getMatchConfidence()` (v6 Smart Address Match) and `getAccuracy()`. +``` + +(Note: the README contains nested code fences — when editing, keep the existing file's +fence style so the Markdown renders correctly.) + +**Step 9.4: Main README** — update the Mapbox row (line ~137) website link: + +``` +[Website](https://www.mapbox.com/geocoding/) → [Website](https://docs.mapbox.com/api/search/geocoding/) +``` + +**Step 9.5: Commit** + +```bash +git add -A src/Provider/Mapbox README.md +git commit -m "chore(mapbox): drop v5 fixtures; docs(mapbox): document v6 upgrade (2.0.0)" +``` + +**Step 9.6: Full verification (CONTRIBUTING.md: "You MUST run the test suite")** + +Run, in order — all must pass with pristine output: +```bash +composer test # full suite, all providers +composer analyse # PHPStan level 6 +composer cs # PHP-CS-Fixer dry-run (if it reports fixes, run composer cs:fix, re-run composer test, and amend/append a commit) +``` + +Fix any PHPStan/CS findings in the touched files (Mapbox.php, MapboxAddress.php, tests) +and re-run until all three commands are clean. + +--- + +## Final state checklist + +- [ ] `Geocoder\Provider\Mapbox\Mapbox` builds v6 forward/reverse/structured requests (deterministic parameter order) +- [ ] v6 responses parsed onto `MapboxAddress` (+ `matchCode`, `matchConfidence`, `accuracy`) +- [ ] `TYPE_POI` / `TYPE_POI_LANDMARK` / `GEOCODING_MODE_*` removed; `TYPE_STREET` / `TYPE_BLOCK` / `TYPE_SECONDARY_ADDRESS` added; `TYPES` updated +- [ ] Constructor 4th arg is `bool $permanent` +- [ ] `fuzzy_match` gone, `autocomplete` / `proximity` / `worldview` supported +- [ ] 7 v6 integration fixtures replayed in CI with the placeholder key; all v5 fixtures deleted +- [ ] Provider CHANGELOG 2.0.0 lists every breaking change +- [ ] Provider README documents v6 usage incl. Structured Input; main README link updated +- [ ] `composer test` / `composer analyse` / `composer cs` all pass +- [ ] Commits are small, meaningful, in TDD order (tests before implementation) From 7ec7d33619acfae70cb8dac13bd21cd818a3ca00 Mon Sep 17 00:00:00 2001 From: David Barratt Date: Fri, 28 Aug 2026 20:36:46 +0000 Subject: [PATCH 02/13] feat(mapbox): add matchCode to MapboxAddress --- src/Provider/Mapbox/Model/MapboxAddress.php | 24 ++++++++++++++ .../Mapbox/Tests/MapboxAddressTest.php | 33 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/Provider/Mapbox/Tests/MapboxAddressTest.php diff --git a/src/Provider/Mapbox/Model/MapboxAddress.php b/src/Provider/Mapbox/Model/MapboxAddress.php index b767f3249..1ed1debe1 100644 --- a/src/Provider/Mapbox/Model/MapboxAddress.php +++ b/src/Provider/Mapbox/Model/MapboxAddress.php @@ -46,6 +46,11 @@ final class MapboxAddress extends Address */ private $neighborhood; + /** + * @var array|null + */ + private $matchCode; + public function withId(?string $id = null): self { $new = clone $this; @@ -132,4 +137,23 @@ public function withNeighborhood(?string $neighborhood = null): self return $new; } + + /** + * @return array|null + */ + public function getMatchCode(): ?array + { + return $this->matchCode; + } + + /** + * @param array|null $matchCode + */ + public function withMatchCode(?array $matchCode = null): self + { + $new = clone $this; + $new->matchCode = $matchCode; + + return $new; + } } diff --git a/src/Provider/Mapbox/Tests/MapboxAddressTest.php b/src/Provider/Mapbox/Tests/MapboxAddressTest.php new file mode 100644 index 000000000..34bd32558 --- /dev/null +++ b/src/Provider/Mapbox/Tests/MapboxAddressTest.php @@ -0,0 +1,33 @@ +build(MapboxAddress::class); + + $this->assertNull($address->getMatchCode()); + + $matchCode = ['street' => 'matched', 'confidence' => 'exact']; + $address = $address->withMatchCode($matchCode); + + $this->assertSame($matchCode, $address->getMatchCode()); + $this->assertNull($address->withMatchCode($matchCode)->withMatchCode()->getMatchCode()); + } +} From c723cdf0c8d8894fb8bf6a248a4bfbf156e34694 Mon Sep 17 00:00:00 2001 From: David Barratt Date: Fri, 28 Aug 2026 20:37:46 +0000 Subject: [PATCH 03/13] feat(mapbox): add matchConfidence to MapboxAddress --- src/Provider/Mapbox/Model/MapboxAddress.php | 18 ++++++++++++++++++ .../Mapbox/Tests/MapboxAddressTest.php | 12 ++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/Provider/Mapbox/Model/MapboxAddress.php b/src/Provider/Mapbox/Model/MapboxAddress.php index 1ed1debe1..b5bb1df87 100644 --- a/src/Provider/Mapbox/Model/MapboxAddress.php +++ b/src/Provider/Mapbox/Model/MapboxAddress.php @@ -51,6 +51,11 @@ final class MapboxAddress extends Address */ private $matchCode; + /** + * @var string|null + */ + private $matchConfidence; + public function withId(?string $id = null): self { $new = clone $this; @@ -156,4 +161,17 @@ public function withMatchCode(?array $matchCode = null): self return $new; } + + public function getMatchConfidence(): ?string + { + return $this->matchConfidence; + } + + public function withMatchConfidence(?string $matchConfidence = null): self + { + $new = clone $this; + $new->matchConfidence = $matchConfidence; + + return $new; + } } diff --git a/src/Provider/Mapbox/Tests/MapboxAddressTest.php b/src/Provider/Mapbox/Tests/MapboxAddressTest.php index 34bd32558..d33b8eb37 100644 --- a/src/Provider/Mapbox/Tests/MapboxAddressTest.php +++ b/src/Provider/Mapbox/Tests/MapboxAddressTest.php @@ -30,4 +30,16 @@ public function testMatchCode(): void $this->assertSame($matchCode, $address->getMatchCode()); $this->assertNull($address->withMatchCode($matchCode)->withMatchCode()->getMatchCode()); } + + public function testMatchConfidence(): void + { + $address = (new AddressBuilder('mapbox'))->build(MapboxAddress::class); + + $this->assertNull($address->getMatchConfidence()); + + $address = $address->withMatchConfidence('exact'); + + $this->assertSame('exact', $address->getMatchConfidence()); + $this->assertNull($address->withMatchConfidence('exact')->withMatchConfidence()->getMatchConfidence()); + } } From af2c3e3c28e6e455ac9da622a732b10e8fc431ee Mon Sep 17 00:00:00 2001 From: David Barratt Date: Fri, 28 Aug 2026 20:38:41 +0000 Subject: [PATCH 04/13] feat(mapbox): add accuracy to MapboxAddress --- src/Provider/Mapbox/Model/MapboxAddress.php | 18 ++++++++++++++++++ .../Mapbox/Tests/MapboxAddressTest.php | 12 ++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/Provider/Mapbox/Model/MapboxAddress.php b/src/Provider/Mapbox/Model/MapboxAddress.php index b5bb1df87..cfb51ad08 100644 --- a/src/Provider/Mapbox/Model/MapboxAddress.php +++ b/src/Provider/Mapbox/Model/MapboxAddress.php @@ -56,6 +56,11 @@ final class MapboxAddress extends Address */ private $matchConfidence; + /** + * @var string|null + */ + private $accuracy; + public function withId(?string $id = null): self { $new = clone $this; @@ -174,4 +179,17 @@ public function withMatchConfidence(?string $matchConfidence = null): self return $new; } + + public function getAccuracy(): ?string + { + return $this->accuracy; + } + + public function withAccuracy(?string $accuracy = null): self + { + $new = clone $this; + $new->accuracy = $accuracy; + + return $new; + } } diff --git a/src/Provider/Mapbox/Tests/MapboxAddressTest.php b/src/Provider/Mapbox/Tests/MapboxAddressTest.php index d33b8eb37..02c0a1171 100644 --- a/src/Provider/Mapbox/Tests/MapboxAddressTest.php +++ b/src/Provider/Mapbox/Tests/MapboxAddressTest.php @@ -42,4 +42,16 @@ public function testMatchConfidence(): void $this->assertSame('exact', $address->getMatchConfidence()); $this->assertNull($address->withMatchConfidence('exact')->withMatchConfidence()->getMatchConfidence()); } + + public function testAccuracy(): void + { + $address = (new AddressBuilder('mapbox'))->build(MapboxAddress::class); + + $this->assertNull($address->getAccuracy()); + + $address = $address->withAccuracy('rooftop'); + + $this->assertSame('rooftop', $address->getAccuracy()); + $this->assertNull($address->withAccuracy('rooftop')->withAccuracy()->getAccuracy()); + } } From b44c3adfb9049e75818edcf68a3372aa4e59da80 Mon Sep 17 00:00:00 2001 From: David Barratt Date: Fri, 28 Aug 2026 20:39:40 +0000 Subject: [PATCH 05/13] test(mapbox): prepare test suite for the v6 API --- src/Provider/Mapbox/Tests/MapboxTest.php | 194 +---------------------- 1 file changed, 8 insertions(+), 186 deletions(-) diff --git a/src/Provider/Mapbox/Tests/MapboxTest.php b/src/Provider/Mapbox/Tests/MapboxTest.php index 82668e672..0d08fcf0d 100644 --- a/src/Provider/Mapbox/Tests/MapboxTest.php +++ b/src/Provider/Mapbox/Tests/MapboxTest.php @@ -13,10 +13,8 @@ namespace Geocoder\Provider\Mapbox\Tests; use Geocoder\IntegrationTest\BaseTestCase; -use Geocoder\Model\AddressCollection; -use Geocoder\Model\Bounds; +use Geocoder\Exception\InvalidServerResponse; use Geocoder\Provider\Mapbox\Mapbox; -use Geocoder\Provider\Mapbox\Model\MapboxAddress; use Geocoder\Query\GeocodeQuery; use Geocoder\Query\ReverseQuery; @@ -72,197 +70,21 @@ public function testGeocodeWithQuotaExceeded(): void $provider->geocodeQuery(GeocodeQuery::create('10 avenue Gambetta, Paris, France')); } - public function testGeocodePlaceWithNoCountryShortCode(): void - { - $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); - - $query = GeocodeQuery::create('princ'); // Principato di Monaco - $query = $query->withLocale('it'); - $query = $query->withBounds(new Bounds( - 35.82809688193029, - -11.36323261153737, - 59.05992036364424, - 34.33947713277206 - )); - $query = $query->withLimit(1); - $query = $query->withData('location_type', [ - Mapbox::TYPE_PLACE, - Mapbox::TYPE_LOCALITY, - Mapbox::TYPE_NEIGHBORHOOD, - Mapbox::TYPE_POI, - Mapbox::TYPE_POI_LANDMARK, - ]); - - $results = $provider->geocodeQuery($query); - - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(1, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertInstanceOf(MapboxAddress::class, $result); - $this->assertEqualsWithDelta(43.73125, $result->getCoordinates()->getLatitude(), 0.001); - $this->assertEqualsWithDelta(7.41974, $result->getCoordinates()->getLongitude(), 0.001); - $this->assertEquals('Principato di Monaco', $result->getStreetName()); - $this->assertEquals('Principato di Monaco', $result->getCountry()->getName()); - $this->assertEquals('place.4899176537126140', $result->getId()); - - // not provided - $this->assertNull($result->getCountry()->getCode()); - $this->assertNull($result->getPostalCode()); - $this->assertNull($result->getLocality()); - $this->assertNull($result->getTimezone()); - $this->assertNull($result->getStreetNumber()); - } - - public function testGeocodeWithRealAddress(): void - { - if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { - $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); - } - - $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); - $results = $provider->geocodeQuery(GeocodeQuery::create('149 9th St, San Francisco, CA 94103')); - - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(5, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertInstanceOf(MapboxAddress::class, $result); - $this->assertEqualsWithDelta(37.77572, $result->getCoordinates()->getLatitude(), 0.001); - $this->assertEqualsWithDelta(-122.41362, $result->getCoordinates()->getLongitude(), 0.001); - $this->assertNull($result->getBounds()); - $this->assertEquals(149, $result->getStreetNumber()); - $this->assertEquals('9th Street', $result->getStreetName()); - $this->assertEquals(94103, $result->getPostalCode()); - $this->assertEquals('San Francisco', $result->getLocality()); - $this->assertEquals('California', $result->getAdminLevels()->get(2)->getName()); - $this->assertEquals('CA', $result->getAdminLevels()->get(2)->getCode()); - $this->assertEquals('San Francisco', $result->getAdminLevels()->get(1)->getName()); - $this->assertEquals('United States', $result->getCountry()->getName()); - $this->assertEquals('US', $result->getCountry()->getCode()); - $this->assertEquals('address.3071152063251042', $result->getId()); - - // not provided - $this->assertNull($result->getTimezone()); - } - - public function testReverse(): void - { - $this->expectException(\Geocoder\Exception\InvalidServerResponse::class); - - $provider = new Mapbox($this->getMockedHttpClient(), 'access_token'); - $provider->reverseQuery(ReverseQuery::fromCoordinates(1, 2)); - } - - public function testReverseWithRealCoordinates(): void - { - if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { - $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); - } - - $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); - $results = $provider->reverseQuery(ReverseQuery::fromCoordinates(48.8631507, 2.388911)); - - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(4, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertInstanceOf(MapboxAddress::class, $result); - $this->assertEquals(8, $result->getStreetNumber()); - $this->assertEquals('Avenue Gambetta', $result->getStreetName()); - $this->assertEquals(75020, $result->getPostalCode()); - $this->assertEquals('Paris', $result->getLocality()); - $this->assertCount(1, $result->getAdminLevels()); - $this->assertEquals('Paris', $result->getAdminLevels()->get(1)->getName()); - $this->assertEquals('France', $result->getCountry()->getName()); - $this->assertEquals('FR', $result->getCountry()->getCode()); - $this->assertEquals('address.1085979616', $result->getId()); - } - public function testGeocodeWithInvalidApiKey(): void { + // The v6 API answers 401 for an invalid token (v5 answered 403). + // Both status codes map to InvalidCredentials in AbstractHttpProvider. $this->expectException(\Geocoder\Exception\InvalidCredentials::class); - $provider = new Mapbox($this->getMockedHttpClient('', 403), 'api_key'); + $provider = new Mapbox($this->getMockedHttpClient('', 401), 'api_key'); $provider->geocodeQuery(GeocodeQuery::create('10 avenue Gambetta, Paris, France')); } - public function testGeocodeWithRealValidApiKey(): void - { - if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { - $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); - } - - $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); - - $results = $provider->geocodeQuery(GeocodeQuery::create('116th St & Broadway, New York, NY 10027, United States')); - - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(5, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertInstanceOf(MapboxAddress::class, $result); - $this->assertEquals('116th Street', $result->getStreetName()); - $this->assertEquals(11356, $result->getPostalCode()); - $this->assertCount(2, $result->getAdminLevels()); - $this->assertEquals('United States', $result->getCountry()->getName()); - $this->assertEquals('US', $result->getCountry()->getCode()); - $this->assertEquals('address.2431617896783536', $result->getId()); - $this->assertNotNull($result->getCoordinates()->getLatitude()); - $this->assertNotNull($result->getCoordinates()->getLongitude()); - $this->assertEqualsWithDelta(40.786596, $result->getCoordinates()->getLatitude(), 0.001); - $this->assertEqualsWithDelta(-73.851157, $result->getCoordinates()->getLongitude(), 0.001); - $this->assertEquals('New York', $result->getLocality()); - $this->assertCount(2, $result->getAdminLevels()); - $this->assertEquals('New York', $result->getAdminLevels()->get(1)->getName()); - $this->assertEquals('NY', $result->getAdminLevels()->get(2)->getCode()); - } - - public function testGeocodeWithFuzzyMatch(): void + public function testReverseWithInvalidResponse(): void { - if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { - $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); - } - - $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); - - $query = GeocodeQuery::create('wahsington'); // Washington - $query = $query->withData('fuzzy_match', true); - $query = $query->withBounds(new Bounds( - 45.54372254, - -124.83609163, - 49.00243912, - -116.91742984 - )); - - $results = $provider->geocodeQuery($query); - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(5, $results); - } - - public function testGeocodeWithoutFuzzyMatch(): void - { - if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { - $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); - } - - $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); + $this->expectException(InvalidServerResponse::class); - $query = GeocodeQuery::create('wahsington'); // Washington - $query = $query->withData('fuzzy_match', false); - $query = $query->withBounds(new Bounds( - 45.54372254, - -124.83609163, - 49.00243912, - -116.91742984 - )); - - $results = $provider->geocodeQuery($query); - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertEmpty($results); + $provider = new Mapbox($this->getMockedHttpClient(), 'access_token'); + $provider->reverseQuery(ReverseQuery::fromCoordinates(1, 2)); } } From a23ffaf04e3ccc5f7195e276fda65f1091027683 Mon Sep 17 00:00:00 2001 From: David Barratt Date: Fri, 28 Aug 2026 20:50:57 +0000 Subject: [PATCH 06/13] test(mapbox): add failing tests for the v6 API --- src/Provider/Mapbox/Mapbox.php | 21 + src/Provider/Mapbox/Tests/MapboxTest.php | 628 +++++++++++++++++++++++ 2 files changed, 649 insertions(+) diff --git a/src/Provider/Mapbox/Mapbox.php b/src/Provider/Mapbox/Mapbox.php index 8d78d7da5..c4d51b90d 100644 --- a/src/Provider/Mapbox/Mapbox.php +++ b/src/Provider/Mapbox/Mapbox.php @@ -95,6 +95,27 @@ final class Mapbox extends AbstractHttpProvider implements Provider */ public const TYPE_ADDRESS = 'address'; + /** + * @var string + */ + public const TYPE_STREET = 'street'; + + /** + * Japanese addresses only. The API accepts this type only for Japan-context requests + * (e.g. with country=jp and language=ja), so it is intentionally not part of TYPES. + * + * @var string + */ + public const TYPE_BLOCK = 'block'; + + /** + * Sub-unit, suite, or lot within a parent address (US only). The API accepts this type + * only for US-context requests, so it is intentionally not part of TYPES. + * + * @var string + */ + public const TYPE_SECONDARY_ADDRESS = 'secondary_address'; + /** * @var string */ diff --git a/src/Provider/Mapbox/Tests/MapboxTest.php b/src/Provider/Mapbox/Tests/MapboxTest.php index 0d08fcf0d..44c2055aa 100644 --- a/src/Provider/Mapbox/Tests/MapboxTest.php +++ b/src/Provider/Mapbox/Tests/MapboxTest.php @@ -14,9 +14,13 @@ use Geocoder\IntegrationTest\BaseTestCase; use Geocoder\Exception\InvalidServerResponse; +use Geocoder\Model\AddressCollection; +use Geocoder\Model\Bounds; use Geocoder\Provider\Mapbox\Mapbox; +use Geocoder\Provider\Mapbox\Model\MapboxAddress; use Geocoder\Query\GeocodeQuery; use Geocoder\Query\ReverseQuery; +use Psr\Http\Message\RequestInterface; class MapboxTest extends BaseTestCase { @@ -87,4 +91,628 @@ public function testReverseWithInvalidResponse(): void $provider = new Mapbox($this->getMockedHttpClient(), 'access_token'); $provider->reverseQuery(ReverseQuery::fromCoordinates(1, 2)); } + + private function assertRequestUrl(string $expectedUrl, callable $request): void + { + $uri = ''; + $client = $this->getMockedHttpClientCallback(function (RequestInterface $request) use (&$uri) { + $uri = (string) $request->getUri(); + }); + + try { + $request($client); + } catch (InvalidServerResponse $e) { + // the mocked client answers an empty body; we only assert the request + } + + $this->assertSame($expectedUrl, $uri); + } + + public function testForwardGeocodeUrl(): void + { + $this->assertRequestUrl( + 'https://api.mapbox.com/search/geocode/v6/forward' + .'?q=10+Downing+St%2C+London' + .'&types=address' + .'&limit=5' + .'&access_token=access_token', + static function ($client) { + (new Mapbox($client, 'access_token'))->geocodeQuery(GeocodeQuery::create('10 Downing St, London')); + } + ); + } + + public function testForwardGeocodeUrlWithAllOptions(): void + { + $this->assertRequestUrl( + 'https://api.mapbox.com/search/geocode/v6/forward' + .'?q=wahsington' + .'&bbox=-124.83609163%2C45.54372254%2C-116.91742984%2C49.00243912' + .'&types=address%2Cstreet' + .'&autocomplete=true' + .'&proximity=-120.09%2C47.60' + .'&worldview=us' + .'&country=US' + .'&language=en' + .'&limit=10' + .'&permanent=true' + .'&access_token=access_token', + static function ($client) { + $provider = new Mapbox($client, 'access_token', 'US', true); + $query = GeocodeQuery::create('wahsington') + ->withLocale('en') + ->withBounds(new Bounds(45.54372254, -124.83609163, 49.00243912, -116.91742984)) + ->withLimit(10) + ->withData('location_type', [Mapbox::TYPE_ADDRESS, Mapbox::TYPE_STREET]) + ->withData('autocomplete', true) + ->withData('proximity', '-120.09,47.60') + ->withData('worldview', 'us'); + $provider->geocodeQuery($query); + } + ); + } + + public function testForwardGeocodeUrlWithStructuredInput(): void + { + // Structured Input: the query text is NOT sent; fields use their v6 names. + // autocomplete defaults to false for structured input (Mapbox guidance). + $this->assertRequestUrl( + 'https://api.mapbox.com/search/geocode/v6/forward' + .'?address_number=2595' + .'&street=Lucky+John+Dr' + .'&place=Park+City' + .'®ion=UT' + .'&postcode=84060' + .'&types=address' + .'&autocomplete=false' + .'&country=US' + .'&limit=5' + .'&access_token=access_token', + static function ($client) { + $provider = new Mapbox($client, 'access_token'); + $query = GeocodeQuery::create('2595 Lucky John Dr, Park City, UT 84060') + ->withData('address_number', '2595') + ->withData('street', 'Lucky John Dr') + ->withData('place', 'Park City') + ->withData('region', 'UT') + ->withData('postcode', '84060') + ->withData('country', 'US'); + $provider->geocodeQuery($query); + } + ); + } + + public function testForwardGeocodeUrlWithStructuredInputAndExplicitAutocomplete(): void + { + // an explicit autocomplete value overrides the structured-input default + $this->assertRequestUrl( + 'https://api.mapbox.com/search/geocode/v6/forward' + .'?street=9th+Street' + .'&types=address' + .'&autocomplete=true' + .'&limit=5' + .'&access_token=access_token', + static function ($client) { + $provider = new Mapbox($client, 'access_token'); + $query = GeocodeQuery::create('9th Street') + ->withData('street', '9th Street') + ->withData('autocomplete', true); + $provider->geocodeQuery($query); + } + ); + } + + public function testReverseGeocodeUrl(): void + { + $this->assertRequestUrl( + 'https://api.mapbox.com/search/geocode/v6/reverse' + .'?longitude=2.388911' + .'&latitude=48.8631507' + .'&types=address' + .'&limit=5' + .'&access_token=access_token', + static function ($client) { + (new Mapbox($client, 'access_token'))->reverseQuery(ReverseQuery::fromCoordinates(48.8631507, 2.388911)); + } + ); + } + + public function testReverseGeocodeUrlWithLocationTypeAndCountry(): void + { + $this->assertRequestUrl( + 'https://api.mapbox.com/search/geocode/v6/reverse' + .'?longitude=2.388911' + .'&latitude=48.8631507' + .'&types=address%2Cpostcode' + .'&country=FR' + .'&limit=2' + .'&access_token=access_token', + static function ($client) { + $provider = new Mapbox($client, 'access_token'); + $query = ReverseQuery::fromCoordinates(48.8631507, 2.388911) + ->withData('location_type', [Mapbox::TYPE_ADDRESS, Mapbox::TYPE_POSTCODE]) + ->withData('country', 'FR') + ->withLimit(2); + $provider->reverseQuery($query); + } + ); + } + + public function testParseForwardAddressFeature(): void + { + $json = <<<'JSON' + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "dXJuOm1ieGFkcjo3Njg3YjZmNy01YmZkLTQzMjItOGMzOS02OGE1NDhmZWYwM2U", + "geometry": { "type": "Point", "coordinates": [-122.413709, 37.7757] }, + "properties": { + "mapbox_id": "dXJuOm1ieGFkcjo3Njg3YjZmNy01YmZkLTQzMjItOGMzOS02OGE1NDhmZWYwM2U", + "feature_type": "address", + "name": "149 9th Street", + "full_address": "149 9th Street, San Francisco, California 94103, United States", + "coordinates": { "longitude": -122.413709, "latitude": 37.7757, "accuracy": "rooftop" }, + "match_code": { "address_number": "matched", "street": "matched", "confidence": "exact" }, + "context": { + "address": { "address_number": "149", "street_name": "9th Street", "name": "149 9th Street" }, + "street": { "name": "9th Street" }, + "neighborhood": { "name": "South of Market" }, + "postcode": { "name": "94103" }, + "place": { "name": "San Francisco" }, + "region": { "name": "California", "region_code": "CA", "region_code_full": "US-CA" }, + "country": { "name": "United States", "country_code": "US", "country_code_alpha_3": "USA" } + } + } + } + ] + } + JSON; + + $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); + $results = $provider->geocodeQuery(GeocodeQuery::create('149 9th St, San Francisco, CA 94103')); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(1, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(MapboxAddress::class, $result); + $this->assertEqualsWithDelta(37.7757, $result->getCoordinates()->getLatitude(), 0.00001); + $this->assertEqualsWithDelta(-122.413709, $result->getCoordinates()->getLongitude(), 0.00001); + $this->assertEquals('dXJuOm1ieGFkcjo3Njg3YjZmNy01YmZkLTQzMjItOGMzOS02OGE1NDhmZWYwM2U', $result->getId()); + $this->assertEquals('9th Street', $result->getStreetName()); + $this->assertEquals('149', $result->getStreetNumber()); + $this->assertEquals('94103', $result->getPostalCode()); + $this->assertEquals('San Francisco', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('San Francisco', $result->getAdminLevels()->get(1)->getName()); + $this->assertEquals('California', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('CA', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('United States', $result->getCountry()->getName()); + $this->assertEquals('US', $result->getCountry()->getCode()); + $this->assertEquals('149 9th Street, San Francisco, California 94103, United States', $result->getFormattedAddress()); + $this->assertEquals('South of Market', $result->getNeighborhood()); + $this->assertEquals(['address'], $result->getResultType()); + $this->assertEquals('exact', $result->getMatchConfidence()); + $this->assertEquals('matched', $result->getMatchCode()['street']); + $this->assertEquals('rooftop', $result->getAccuracy()); + $this->assertNull($result->getTimezone()); + } + + public function testParseForwardPlaceFeature(): void + { + // place feature: no context.address; name is the place name; region code may be 3 letters + $json = <<<'JSON' + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "dXJuOm1ieHBsYzpoMmhQ", + "geometry": { "type": "Point", "coordinates": [-0.83069, 51.724422] }, + "properties": { + "mapbox_id": "dXJuOm1ieHBsYzpoMmhQ", + "feature_type": "place", + "name": "Princes Risborough", + "full_address": "Princes Risborough, Buckinghamshire, Inghilterra, Regno Unito", + "coordinates": { "longitude": -0.83069, "latitude": 51.724422 }, + "context": { + "place": { "name": "Princes Risborough" }, + "region": { "name": "Inghilterra", "region_code": "ENG" }, + "country": { "name": "Regno Unito", "country_code": "GB" } + } + } + } + ] + } + JSON; + + $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); + $results = $provider->geocodeQuery(GeocodeQuery::create('princ')); + + $this->assertCount(1, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertEquals('dXJuOm1ieHBsYzpoMmhQ', $result->getId()); + $this->assertEquals('Princes Risborough', $result->getStreetName()); + $this->assertNull($result->getStreetNumber()); + $this->assertNull($result->getPostalCode()); + $this->assertEquals('Princes Risborough', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('Princes Risborough', $result->getAdminLevels()->get(1)->getName()); + $this->assertEquals('Inghilterra', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('ENG', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('Regno Unito', $result->getCountry()->getName()); + $this->assertEquals('GB', $result->getCountry()->getCode()); + $this->assertEquals('Princes Risborough, Buckinghamshire, Inghilterra, Regno Unito', $result->getFormattedAddress()); + $this->assertNull($result->getNeighborhood()); + $this->assertEquals(['place'], $result->getResultType()); + $this->assertNull($result->getMatchConfidence()); + $this->assertNull($result->getMatchCode()); + $this->assertNull($result->getAccuracy()); + } + + public function testParseForwardStreetFeature(): void + { + // v6 street feature: street name comes from context.street + $json = <<<'JSON' + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "address.7607992201284362", + "geometry": { "type": "Point", "coordinates": [-117.039767, 46.930253] }, + "properties": { + "mapbox_id": "address.7607992201284362", + "feature_type": "street", + "name": "Washington Highway 6", + "full_address": "Washington Highway 6, Palouse, Washington 99161, United States", + "coordinates": { "longitude": -117.039767, "latitude": 46.930253 }, + "context": { + "street": { "name": "Washington Highway 6" }, + "postcode": { "name": "99161" }, + "place": { "name": "Palouse" }, + "region": { "name": "Washington", "region_code": "WA" }, + "country": { "name": "United States", "country_code": "US" } + } + } + } + ] + } + JSON; + + $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); + $results = $provider->geocodeQuery(GeocodeQuery::create('Washington Highway 6')); + + $this->assertCount(1, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertEquals('address.7607992201284362', $result->getId()); + $this->assertEquals('Washington Highway 6', $result->getStreetName()); + $this->assertNull($result->getStreetNumber()); + $this->assertEquals('99161', $result->getPostalCode()); + $this->assertEquals('Palouse', $result->getLocality()); + $this->assertEquals('Washington', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('WA', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals(['street'], $result->getResultType()); + } + + public function testParseFeatureWithoutContext(): void + { + // features without a context object must still parse (name + coordinates) + $json = <<<'JSON' + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "abc123", + "geometry": { "type": "Point", "coordinates": [2.35, 48.85] }, + "properties": { + "mapbox_id": "abc123", + "feature_type": "place", + "name": "Some Place", + "coordinates": { "longitude": 2.35, "latitude": 48.85 } + } + } + ] + } + JSON; + + $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); + $results = $provider->geocodeQuery(GeocodeQuery::create('Some Place')); + + $this->assertCount(1, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertEquals('Some Place', $result->getStreetName()); + $this->assertEquals('abc123', $result->getId()); + $this->assertEqualsWithDelta(48.85, $result->getCoordinates()->getLatitude(), 0.00001); + $this->assertEqualsWithDelta(2.35, $result->getCoordinates()->getLongitude(), 0.00001); + } + + public function testParseEmptyFeatures(): void + { + $json = '{"type":"FeatureCollection","features":[]}'; + + $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); + $results = $provider->geocodeQuery(GeocodeQuery::create('jsajhgsdkfjhsfkjhaldkadjaslgldasd')); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertEmpty($results); + } + + public function testParseInvalidJson(): void + { + $this->expectException(InvalidServerResponse::class); + + $provider = new Mapbox($this->getMockedHttpClient('{invalid json'), 'access_token'); + $provider->geocodeQuery(GeocodeQuery::create('10 Downing St, London')); + } + + public function testGeocodeWithRealAddress(): void + { + if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { + $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); + } + + $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); + $results = $provider->geocodeQuery(GeocodeQuery::create('149 9th St, San Francisco, CA 94103')); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(5, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(MapboxAddress::class, $result); + $this->assertEqualsWithDelta(37.7757, $result->getCoordinates()->getLatitude(), 0.001); + $this->assertEqualsWithDelta(-122.413709, $result->getCoordinates()->getLongitude(), 0.001); + $this->assertEquals('9th Street', $result->getStreetName()); + $this->assertEquals('149', $result->getStreetNumber()); + $this->assertEquals('94103', $result->getPostalCode()); + $this->assertEquals('San Francisco', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('California', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('CA', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('United States', $result->getCountry()->getName()); + $this->assertEquals('US', $result->getCountry()->getCode()); + $this->assertEquals('149 9th Street, San Francisco, California 94103, United States', $result->getFormattedAddress()); + $this->assertEquals('South of Market', $result->getNeighborhood()); + $this->assertEquals(['address'], $result->getResultType()); + $this->assertEquals('exact', $result->getMatchConfidence()); + $this->assertEquals('rooftop', $result->getAccuracy()); + + // not provided + $this->assertNull($result->getTimezone()); + } + + public function testGeocodeWithRealAddressInDc(): void + { + if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { + $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); + } + + $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); + $results = $provider->geocodeQuery(GeocodeQuery::create('1600 Pennsylvania Avenue NW, Washington, DC 20500')); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(2, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(MapboxAddress::class, $result); + $this->assertEqualsWithDelta(38.897684, $result->getCoordinates()->getLatitude(), 0.001); + $this->assertEqualsWithDelta(-77.036574, $result->getCoordinates()->getLongitude(), 0.001); + $this->assertEquals('Pennsylvania Avenue Northwest', $result->getStreetName()); + $this->assertEquals('1600', $result->getStreetNumber()); + $this->assertEquals('20500', $result->getPostalCode()); + $this->assertEquals('Washington', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('District of Columbia', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('DC', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('United States', $result->getCountry()->getName()); + $this->assertEquals('US', $result->getCountry()->getCode()); + $this->assertEquals('1600 Pennsylvania Avenue Northwest, Washington, District of Columbia 20500, United States', $result->getFormattedAddress()); + $this->assertEquals('exact', $result->getMatchConfidence()); + $this->assertEquals('rooftop', $result->getAccuracy()); + } + + public function testReverseWithRealCoordinates(): void + { + if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { + $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); + } + + $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); + $results = $provider->reverseQuery(ReverseQuery::fromCoordinates(48.8631507, 2.388911)); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(4, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(MapboxAddress::class, $result); + $this->assertEquals('12', $result->getStreetNumber()); + $this->assertEquals('Avenue Gambetta', $result->getStreetName()); + $this->assertEquals('75020', $result->getPostalCode()); + $this->assertEquals('Paris', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('Paris', $result->getAdminLevels()->get(1)->getName()); + $this->assertEquals('Île-de-France', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('IDF', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('France', $result->getCountry()->getName()); + $this->assertEquals('FR', $result->getCountry()->getCode()); + $this->assertEquals('12 Avenue Gambetta, 75020 Paris, France', $result->getFormattedAddress()); + $this->assertEquals('rooftop', $result->getAccuracy()); + + // not provided by reverse geocoding + $this->assertNull($result->getMatchConfidence()); + $this->assertNull($result->getNeighborhood()); + } + + public function testGeocodePlaceWithLocale(): void + { + if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { + $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); + } + + $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); + + $query = GeocodeQuery::create('princ'); + $query = $query->withLocale('it'); + $query = $query->withBounds(new Bounds( + 35.82809688193029, + -11.36323261153737, + 59.05992036364424, + 34.33947713277206 + )); + $query = $query->withLimit(1); + $query = $query->withData('location_type', [ + Mapbox::TYPE_PLACE, + Mapbox::TYPE_LOCALITY, + Mapbox::TYPE_NEIGHBORHOOD, + ]); + + $results = $provider->geocodeQuery($query); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(1, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(MapboxAddress::class, $result); + $this->assertEqualsWithDelta(51.724422, $result->getCoordinates()->getLatitude(), 0.001); + $this->assertEqualsWithDelta(-0.83069, $result->getCoordinates()->getLongitude(), 0.001); + $this->assertEquals('Princes Risborough', $result->getStreetName()); + $this->assertEquals(['place'], $result->getResultType()); + $this->assertEquals('Princes Risborough', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('Princes Risborough', $result->getAdminLevels()->get(1)->getName()); + $this->assertEquals('Inghilterra', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('ENG', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('Regno Unito', $result->getCountry()->getName()); + $this->assertEquals('GB', $result->getCountry()->getCode()); + + // not provided + $this->assertNull($result->getPostalCode()); + $this->assertNull($result->getStreetNumber()); + $this->assertNull($result->getMatchConfidence()); + } + + public function testGeocodeWithAutocompleteEnabled(): void + { + if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { + $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); + } + + $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); + + $query = GeocodeQuery::create('washi'); + $query = $query->withData('autocomplete', true); + $query = $query->withBounds(new Bounds( + 45.54372254, + -124.83609163, + 49.00243912, + -116.91742984 + )); + $query = $query->withData('location_type', [ + Mapbox::TYPE_REGION, + Mapbox::TYPE_NEIGHBORHOOD, + Mapbox::TYPE_STREET, + Mapbox::TYPE_PLACE, + Mapbox::TYPE_LOCALITY, + ]); + + $results = $provider->geocodeQuery($query); + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(5, $results); + + /** @var MapboxAddress $first */ + $first = $results->first(); + $this->assertEquals(['region'], $first->getResultType()); + $this->assertEquals('Washington', $first->getStreetName()); + $this->assertEquals('Washington', $first->getAdminLevels()->get(2)->getName()); + $this->assertEquals('WA', $first->getAdminLevels()->get(2)->getCode()); + } + + public function testGeocodeWithAutocompleteDisabled(): void + { + if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { + $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); + } + + $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); + + $query = GeocodeQuery::create('washi'); + $query = $query->withData('autocomplete', false); + $query = $query->withBounds(new Bounds( + 45.54372254, + -124.83609163, + 49.00243912, + -116.91742984 + )); + $query = $query->withData('location_type', [ + Mapbox::TYPE_REGION, + Mapbox::TYPE_NEIGHBORHOOD, + Mapbox::TYPE_STREET, + Mapbox::TYPE_PLACE, + Mapbox::TYPE_LOCALITY, + ]); + + $results = $provider->geocodeQuery($query); + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(1, $results); + + /** @var MapboxAddress $first */ + $first = $results->first(); + $this->assertEquals(['street'], $first->getResultType()); + $this->assertEquals('Yashi Road', $first->getStreetName()); + } + + public function testGeocodeWithStructuredInput(): void + { + if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { + $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); + } + + $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); + + $query = GeocodeQuery::create('2595 Lucky John Dr, Park City, UT 84060'); + $query = $query->withData('address_number', '2595'); + $query = $query->withData('street', 'Lucky John Dr'); + $query = $query->withData('place', 'Park City'); + $query = $query->withData('region', 'UT'); + $query = $query->withData('postcode', '84060'); + + $results = $provider->geocodeQuery($query); + + $this->assertInstanceOf(AddressCollection::class, $results); + $this->assertCount(1, $results); + + /** @var MapboxAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(MapboxAddress::class, $result); + $this->assertEqualsWithDelta(40.6716, $result->getCoordinates()->getLatitude(), 0.001); + $this->assertEqualsWithDelta(-111.507008, $result->getCoordinates()->getLongitude(), 0.001); + $this->assertEquals('Lucky John Drive', $result->getStreetName()); + $this->assertEquals('2595', $result->getStreetNumber()); + $this->assertEquals('84060', $result->getPostalCode()); + $this->assertEquals('Park City', $result->getLocality()); + $this->assertCount(2, $result->getAdminLevels()); + $this->assertEquals('Park City', $result->getAdminLevels()->get(1)->getName()); + $this->assertEquals('Utah', $result->getAdminLevels()->get(2)->getName()); + $this->assertEquals('UT', $result->getAdminLevels()->get(2)->getCode()); + $this->assertEquals('United States', $result->getCountry()->getName()); + $this->assertEquals('US', $result->getCountry()->getCode()); + $this->assertEquals('2595 Lucky John Drive, Park City, Utah 84060, United States', $result->getFormattedAddress()); + $this->assertEquals('Park Meadows', $result->getNeighborhood()); + $this->assertEquals('exact', $result->getMatchConfidence()); + $this->assertEquals('rooftop', $result->getAccuracy()); + } } From 769d0b5a777137d1cfb59e01d783db8301845860 Mon Sep 17 00:00:00 2001 From: David Barratt Date: Fri, 28 Aug 2026 21:10:43 +0000 Subject: [PATCH 07/13] feat(mapbox): build requests for the Geocoding API v6 --- src/Provider/Mapbox/Mapbox.php | 215 ++++++++++++++++++++------------- 1 file changed, 132 insertions(+), 83 deletions(-) diff --git a/src/Provider/Mapbox/Mapbox.php b/src/Provider/Mapbox/Mapbox.php index c4d51b90d..6ffdb1762 100644 --- a/src/Provider/Mapbox/Mapbox.php +++ b/src/Provider/Mapbox/Mapbox.php @@ -13,7 +13,6 @@ namespace Geocoder\Provider\Mapbox; use Geocoder\Collection; -use Geocoder\Exception\InvalidArgument; use Geocoder\Exception\InvalidServerResponse; use Geocoder\Exception\UnsupportedOperation; use Geocoder\Http\Provider\AbstractHttpProvider; @@ -30,30 +29,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 FORWARD_ENDPOINT_URL = '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'; - - /** - * @var string - */ - public const GEOCODING_MODE_PLACES = 'mapbox.places'; - - /** - * @var string - */ - public const GEOCODING_MODE_PLACES_PERMANENT = 'mapbox.places-permanent'; - - /** - * @var string[] - */ - public const GEOCODING_MODES = [ - self::GEOCODING_MODE_PLACES, - self::GEOCODING_MODE_PLACES_PERMANENT, - ]; + public const REVERSE_ENDPOINT_URL = 'https://api.mapbox.com/search/geocode/v6/reverse'; /** * @var string @@ -116,16 +97,6 @@ final class Mapbox extends AbstractHttpProvider implements Provider */ public const TYPE_SECONDARY_ADDRESS = 'secondary_address'; - /** - * @var string - */ - public const TYPE_POI = 'poi'; - - /** - * @var string - */ - public const TYPE_POI_LANDMARK = 'poi.landmark'; - /** * @var string[] */ @@ -137,9 +108,25 @@ 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, + ]; + + /** + * Query data keys that trigger the v6 Structured Input mode (the query text is not sent). + * + * @var string[] + */ + public const STRUCTURED_INPUT_FIELDS = [ + 'address_line1', + 'address_number', + 'street', + 'block', + 'place', + 'region', + 'postcode', + 'locality', + 'neighborhood', ]; /** @@ -158,91 +145,143 @@ final class Mapbox extends AbstractHttpProvider implements Provider private $country; /** - * @var string + * @var bool */ - private $geocodingMode; + private $permanent; /** * @param ClientInterface $client An HTTP adapter * @param string $accessToken Your Mapbox access token + * @param string|null $country Restrict results to one or more ISO 3166 alpha-2 countries (comma separated) + * @param bool $permanent Store results permanently (v6 `permanent` parameter) */ public function __construct( ClientInterface $client, string $accessToken, ?string $country = null, - string $geocodingMode = self::GEOCODING_MODE_PLACES, + bool $permanent = false, ) { parent::__construct($client); - if (!in_array($geocodingMode, self::GEOCODING_MODES)) { - throw new InvalidArgument('The Mapbox geocoding mode should be either mapbox.places or mapbox.places-permanent.'); - } - $this->accessToken = $accessToken; $this->country = $country; - $this->geocodingMode = $geocodingMode; + $this->permanent = $permanent; } public function geocodeQuery(GeocodeQuery $query): Collection { - // Mapbox API returns invalid data if IP address given - // This API doesn't handle IPs + // The Mapbox API does not geocode raw IP addresses if (filter_var($query->getText(), FILTER_VALIDATE_IP)) { 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())); + $parameters = []; + + // v6 Structured Input: typed fields replace the `q` search text + $structured = $this->structuredInputFields($query); + if ([] !== $structured) { + foreach (self::STRUCTURED_INPUT_FIELDS as $field) { + if (isset($structured[$field])) { + $parameters[$field] = $structured[$field]; + } + } + } else { + $parameters['q'] = $query->getText(); + } - $urlParameters = []; if ($query->getBounds()) { // Format is "minLon,minLat,maxLon,maxLat" - $urlParameters['bbox'] = sprintf( + $parameters['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()) ); } - if (null !== $locationType = $query->getData('location_type')) { - $urlParameters['types'] = is_array($locationType) ? implode(',', $locationType) : $locationType; - } else { - $urlParameters['types'] = self::DEFAULT_TYPE; + $parameters['types'] = $this->locationTypes($query); + + if (null !== $autocomplete = $query->getData('autocomplete')) { + $parameters['autocomplete'] = $autocomplete ? 'true' : 'false'; + } elseif ([] !== $structured) { + // Mapbox recommends autocomplete=false for structured input + $parameters['autocomplete'] = 'false'; } - if (null !== $fuzzyMatch = $query->getData('fuzzy_match')) { - $urlParameters['fuzzyMatch'] = $fuzzyMatch ? 'true' : 'false'; + if (null !== $proximity = $query->getData('proximity')) { + $parameters['proximity'] = $proximity; } - if (count($urlParameters) > 0) { - $url .= '?'.http_build_query($urlParameters); + if (null !== $worldview = $query->getData('worldview')) { + $parameters['worldview'] = $worldview; } - return $this->fetchUrl($url, $query->getLimit(), $query->getLocale(), $query->getData('country', $this->country)); + return $this->fetchUrl( + self::FORWARD_ENDPOINT_URL, + $query->getLimit(), + $query->getLocale(), + $query->getData('country', $this->country), + $parameters + ); } public function reverseQuery(ReverseQuery $query): Collection { $coordinate = $query->getCoordinates(); - $url = sprintf( - self::REVERSE_ENDPOINT_URL_SSL, - $this->geocodingMode, - $coordinate->getLongitude(), - $coordinate->getLatitude() + $parameters = [ + 'longitude' => $this->formatCoordinate($coordinate->getLongitude()), + 'latitude' => $this->formatCoordinate($coordinate->getLatitude()), + ]; + + $parameters['types'] = $this->locationTypes($query); + + return $this->fetchUrl( + self::REVERSE_ENDPOINT_URL, + $query->getLimit(), + $query->getLocale(), + $query->getData('country', $this->country), + $parameters ); + } - if (null !== $locationType = $query->getData('location_type')) { - $urlParameters['types'] = is_array($locationType) ? implode(',', $locationType) : $locationType; - } else { - $urlParameters['types'] = self::DEFAULT_TYPE; + /** + * @return array + */ + 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; + } } - if (count($urlParameters) > 0) { - $url .= '?'.http_build_query($urlParameters); + return $fields; + } + + /** + * @param GeocodeQuery|ReverseQuery $query + */ + private function locationTypes($query): string + { + if (null !== $locationType = $query->getData('location_type')) { + return is_array($locationType) ? implode(',', $locationType) : $locationType; } - return $this->fetchUrl($url, $query->getLimit(), $query->getLocale(), $query->getData('country', $this->country)); + return self::DEFAULT_TYPE; + } + + /** + * Format a coordinate with the shortest decimal representation that round-trips the + * value exactly (a plain (string) cast truncates to the 14 significant digits of the + * default `precision` ini setting, which would change the request URL). + */ + private function formatCoordinate(float $value): string + { + return (string) json_encode($value); } public function getName(): string @@ -251,25 +290,35 @@ public function getName(): string } /** - * @return string query with extra params + * @param array $parameters */ - private function buildQuery(string $url, int $limit, ?string $locale = null, ?string $country = null): string + private function buildQuery(string $url, int $limit, ?string $locale, ?string $country, array $parameters): string { - $parameters = array_filter([ - 'country' => $country, - 'language' => $locale, - 'limit' => $limit, - 'access_token' => $this->accessToken, - ]); + if (null !== $country) { + $parameters['country'] = $country; + } + + if (null !== $locale) { + $parameters['language'] = $locale; + } + + $parameters['limit'] = $limit; - $separator = parse_url($url, PHP_URL_QUERY) ? '&' : '?'; + if ($this->permanent) { + $parameters['permanent'] = 'true'; + } + + $parameters['access_token'] = $this->accessToken; - return $url.$separator.http_build_query($parameters); + return $url.'?'.http_build_query($parameters); } - private function fetchUrl(string $url, int $limit, ?string $locale = null, ?string $country = null): AddressCollection + /** + * @param array $parameters + */ + private function fetchUrl(string $url, int $limit, ?string $locale, ?string $country, array $parameters): AddressCollection { - $url = $this->buildQuery($url, $limit, $locale, $country); + $url = $this->buildQuery($url, $limit, $locale, $country, $parameters); $content = $this->getUrlContents($url); $json = $this->validateResponse($url, $content); From 174081d875709e0ef520465b1f6ddef569b0fa82 Mon Sep 17 00:00:00 2001 From: David Barratt Date: Fri, 28 Aug 2026 21:16:36 +0000 Subject: [PATCH 08/13] feat(mapbox): parse Geocoding API v6 responses --- src/Provider/Mapbox/Mapbox.php | 131 +++++++++++++++++++-------------- 1 file changed, 77 insertions(+), 54 deletions(-) diff --git a/src/Provider/Mapbox/Mapbox.php b/src/Provider/Mapbox/Mapbox.php index 6ffdb1762..2b8a34221 100644 --- a/src/Provider/Mapbox/Mapbox.php +++ b/src/Provider/Mapbox/Mapbox.php @@ -329,103 +329,126 @@ private function fetchUrl(string $url, int $limit, ?string $locale, ?string $cou $results = []; foreach ($json['features'] as $result) { - if (!array_key_exists('context', $result)) { + $results[] = $this->parseFeature($result); + + if (count($results) >= $limit) { break; } + } - $builder = new AddressBuilder($this->getName()); - $this->parseCoordinates($builder, $result); + return new AddressCollection($results); + } - // set official Mapbox place id - if (isset($result['id'])) { - $builder->setValue('id', $result['id']); - } + /** + * @param array $result + */ + private function parseFeature(array $result): MapboxAddress + { + $properties = $result['properties'] ?? []; + $context = $properties['context'] ?? []; - // set official Mapbox place id - if (isset($result['text'])) { - $builder->setValue('street_name', $result['text']); - } + $builder = new AddressBuilder($this->getName()); + $this->parseCoordinates($builder, $result); - // update address components - foreach ($result['context'] as $component) { - $this->updateAddressComponent($builder, $component['id'], $component); + // update address components (context is an object keyed by feature type in v6) + foreach ($context as $type => $component) { + if (is_array($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']); - } - if (isset($result['place_type'])) { - $address = $address->withResultType($result['place_type']); - } - if (isset($result['place_name'])) { - $address = $address->withFormattedAddress($result['place_name']); - } - $address = $address->withStreetName($builder->getValue('street_name')); - $address = $address->withNeighborhood($builder->getValue('neighborhood')); - $results[] = $address; + /** @var MapboxAddress $address */ + $address = $builder->build(MapboxAddress::class); - if (count($results) >= $limit) { - break; + if (isset($properties['mapbox_id'])) { + $address = $address->withId((string) $properties['mapbox_id']); + } elseif (isset($result['id'])) { + $address = $address->withId((string) $result['id']); + } + + // street name without the house number (v5 semantics) + $streetName = $context['address']['street_name'] ?? $context['street']['name'] ?? $properties['name'] ?? null; + if (null !== $streetName) { + $address = $address->withStreetName((string) $streetName); + } + + $streetNumber = $context['address']['address_number'] ?? null; + if (null !== $streetNumber) { + $address = $address->withStreetNumber((string) $streetNumber); + } + + if (isset($properties['feature_type'])) { + $address = $address->withResultType([(string) $properties['feature_type']]); + } + + if (isset($properties['full_address'])) { + $address = $address->withFormattedAddress((string) $properties['full_address']); + } + + $neighborhood = $context['neighborhood']['name'] ?? null; + if (null !== $neighborhood && '' !== $neighborhood) { + $address = $address->withNeighborhood((string) $neighborhood); + } + + if (isset($properties['match_code']) && is_array($properties['match_code'])) { + $address = $address->withMatchCode($properties['match_code']); + $confidence = $properties['match_code']['confidence'] ?? null; + if (null !== $confidence) { + $address = $address->withMatchConfidence((string) $confidence); } } - return new AddressCollection($results); + if (isset($properties['coordinates']['accuracy'])) { + $address = $address->withAccuracy((string) $properties['coordinates']['accuracy']); + } + + return $address; } /** * Update current resultSet with given key/value. * - * @param string $type Component type - * @param array $value Component value + * @param array $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']) || '' === $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, neighborhood: handled (or intentionally ignored) elsewhere } } From c4b3ff1e62fb35e23095e0c7eb70c22f19147d45 Mon Sep 17 00:00:00 2001 From: David Barratt Date: Fri, 28 Aug 2026 21:23:01 +0000 Subject: [PATCH 09/13] chore(mapbox): drop v5 fixtures; docs(mapbox): document v6 upgrade (2.0.0) --- README.md | 2 +- src/Provider/Mapbox/CHANGELOG.md | 36 +++++++++++++++++ src/Provider/Mapbox/Readme.md | 40 +++++++++++++++++++ ...m_06551ee827f3b9635793737514ec6b9d368075c4 | 1 + ...m_20d856dd6d6ae671b181193de4e52f7fb651fd97 | 1 - ...m_338a05e2ceeb9a75e55774746945065683127870 | 1 + ...m_436e84f1a3531213304e67e5bca76e7550349a2a | 1 - ...m_5e373c440fdae88e9ec714d42d85489c9986ab7c | 1 - ...m_5e53474a50aa75267953e6d90b2e3472021efbf2 | 1 + ...m_61c96e256b090c50a0a9a247c3808fb84b79f55c | 1 - ...m_7b8b47952b295102acd4cb1bc0a9536c26c7bc9b | 1 + ...m_9e3e5555aca6117bf14dbd86c37ed23cbece335d | 1 + ...m_bbef3c8c03f148c252a89f0a36e1872e67c9a761 | 1 - ...m_c2fd1618bb5f29f1998244add688fc1668ec2416 | 1 - ...m_c97f9d6eb99ff44a7e13d0dc8eb54c3fd8f6971b | 1 - ...m_d72ced1f54dff6389745d11b002a772451804618 | 1 - ...m_d73a2016dbd70a152bffadf02f4f04d7e4d6a0b7 | 1 + ...m_eb032a2644f68b47a4076ea601305eaf8ba72a31 | 1 - ...m_f212d067263ee5a25cce4e79ee710e3a4af3fc5e | 1 - ...m_fee388586d54f2150af453c6e033e452d85e46a8 | 1 + 20 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_06551ee827f3b9635793737514ec6b9d368075c4 delete mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_20d856dd6d6ae671b181193de4e52f7fb651fd97 create mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_338a05e2ceeb9a75e55774746945065683127870 delete mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_436e84f1a3531213304e67e5bca76e7550349a2a delete mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_5e373c440fdae88e9ec714d42d85489c9986ab7c create mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_5e53474a50aa75267953e6d90b2e3472021efbf2 delete mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_61c96e256b090c50a0a9a247c3808fb84b79f55c create mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_7b8b47952b295102acd4cb1bc0a9536c26c7bc9b create mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_9e3e5555aca6117bf14dbd86c37ed23cbece335d delete mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_bbef3c8c03f148c252a89f0a36e1872e67c9a761 delete mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_c2fd1618bb5f29f1998244add688fc1668ec2416 delete mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_c97f9d6eb99ff44a7e13d0dc8eb54c3fd8f6971b delete mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_d72ced1f54dff6389745d11b002a772451804618 create mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_d73a2016dbd70a152bffadf02f4f04d7e4d6a0b7 delete mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_eb032a2644f68b47a4076ea601305eaf8ba72a31 delete mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_f212d067263ee5a25cce4e79ee710e3a4af3fc5e create mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_fee388586d54f2150af453c6e033e452d85e46a8 diff --git a/README.md b/README.md index 31b250196..45f1f47b5 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Provider | Package | Features | Stats [GraphHopper](https://github.com/geocoder-php/graphhopper-provider) | `geocoder-php/graphhopper-provider` | address, reverse
[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)
[![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
[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)
[![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
[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)
[![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
[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)
[![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
[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)
[![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
[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)
[![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
[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)
[![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)
(OpenStreetMap) | `geocoder-php/nominatim-provider` | address, reverse
[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)
[![Total Downloads](https://poser.pugx.org/geocoder-php/nominatim-provider/downloads)](https://packagist.org/packages/geocoder-php/nominatim-provider) diff --git a/src/Provider/Mapbox/CHANGELOG.md b/src/Provider/Mapbox/CHANGELOG.md index 214529e6a..dd0fbf7ce 100644 --- a/src/Provider/Mapbox/CHANGELOG.md +++ b/src/Provider/Mapbox/CHANGELOG.md @@ -2,6 +2,42 @@ 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`) +- v6 Structured Input: forward queries built from typed address fields via query data — `address_line1`, `address_number`, `street`, `block`, `place`, `region`, `postcode`, `locality`, `neighborhood`. When any of these is present, the query text is not sent. +- `Mapbox::TYPE_STREET` (new v6 type, added to `Mapbox::TYPES`) +- `Mapbox::TYPE_BLOCK` (Japan) and `Mapbox::TYPE_SECONDARY_ADDRESS` (US) constants (not part of `TYPES`: the API only accepts them in the corresponding regional context) +- `Mapbox::STRUCTURED_INPUT_FIELDS` constant +- Query data support: `autocomplete` (replaces `fuzzy_match`), `proximity` (`"lon,lat"` or `"ip"`), `worldview` +- New 4th constructor argument `bool $permanent` (v6 `permanent=true` result storage) +- `MapboxAddress::getMatchCode()` / `withMatchCode()` — v6 Smart Address Match object +- `MapboxAddress::getMatchConfidence()` / `withMatchConfidence()` — `exact`, `high`, `medium` or `low` +- `MapboxAddress::getAccuracy()` / `withAccuracy()` — `rooftop`, `parcel`, `point`, `interpolated`, `approximate` or `intersection` + +### Removed + +- Support for the Mapbox Geocoding API v5 (`/geocoding/v5/...`) — pin to `geocoder-php/mapbox-provider:^1.5` to stay on v5 +- `Mapbox::GEOCODE_ENDPOINT_URL_SSL` and `Mapbox::REVERSE_ENDPOINT_URL_SSL` constants (replaced by `Mapbox::FORWARD_ENDPOINT_URL` / `Mapbox::REVERSE_ENDPOINT_URL`) +- `Mapbox::GEOCODING_MODE_PLACES`, `Mapbox::GEOCODING_MODE_PLACES_PERMANENT` and `Mapbox::GEOCODING_MODES` constants (v6 has no geocoding modes; use the `permanent` constructor argument) +- `Mapbox::TYPE_POI` and `Mapbox::TYPE_POI_LANDMARK` constants (v6 does not geocode POIs; passing them as `location_type` now yields a 422 error from the API) +- `fuzzy_match` query data (v6 has no fuzzy/typo matching; use `autocomplete` for prefix-style matching) + +### Changed + +- The 4th constructor argument is now `bool $permanent = false` (was `string $geocodingMode`); passing a mode string changes behavior or errors +- `Mapbox::TYPES` no longer contains the POI types and now contains `street` +- Forward requests send the search text as the `q` query parameter instead of in the URL path +- Reverse requests send `longitude` / `latitude` query parameters (was `{lon},{lat}` in the path) +- `MapboxAddress::getId()` returns the v6 `mapbox_id` value (format changed, e.g. `dXJu...` instead of `address.123`) +- `MapboxAddress::getResultType()` values follow the v6 `feature_type` vocabulary (`street` added, `poi`/`poi.landmark` gone) +- Reverse geocoding with a `limit` greater than 1 requires a single `location_type` value (v6 API constraint: multiple types + limit returns a 422 error) +- Forward `limit` is capped at 10 by the v6 API (higher values are silently clamped) +- Invalid access tokens are now answered with HTTP 401 by the API (v5 used 403); both map to `Geocoder\Exception\InvalidCredentials` +- Geocoded data reflects the v6 dataset: coordinates, results and admin-level codes may differ from v5 + ## 1.5.0 ### Added diff --git a/src/Provider/Mapbox/Readme.md b/src/Provider/Mapbox/Readme.md index ebfbe29e6..d9815823d 100644 --- a/src/Provider/Mapbox/Readme.md +++ b/src/Provider/Mapbox/Readme.md @@ -20,6 +20,46 @@ composer require geocoder-php/mapbox-provider A valid `Access Token` is required for Mapbox. +### Usage + +This provider targets the **Mapbox Geocoding API v6**. If you rely on the v5 API, pin +`geocoder-php/mapbox-provider` to `^1.5`. + +```php +$provider = new Mapbox($client, $accessToken); +// optional: country filter (ISO 3166 alpha-2) and permanent result storage +$provider = new Mapbox($client, $accessToken, 'US', true); + +// Forward geocoding +$provider->geocodeQuery(GeocodeQuery::create('149 9th St, San Francisco, CA 94103')); + +// Forward geocoding with options (query data) +$query = GeocodeQuery::create('washi') + ->withData('autocomplete', true) // prefix-style matching (on by default) + ->withData('location_type', Mapbox::TYPE_STREET) // or an array of Mapbox::TYPE_* constants + ->withData('proximity', '-120.09,47.60') // or 'ip' + ->withData('worldview', 'us') + ->withData('country', 'US'); + +// v6 Structured Input: typed fields replace the free-text query (the text is not sent) +$query = GeocodeQuery::create('2595 Lucky John Dr, Park City, UT 84060') + ->withData('address_number', '2595') + ->withData('street', 'Lucky John Dr') + ->withData('place', 'Park City') + ->withData('region', 'UT') + ->withData('postcode', '84060'); +// Any of address_line1, address_number, street, block, place, region, postcode, +// locality, neighborhood triggers Structured Input; autocomplete defaults to false. + +// Reverse geocoding +$provider->reverseQuery(ReverseQuery::fromCoordinates(48.8631507, 2.388911)); +``` + +Results are `Geocoder\Provider\Mapbox\Model\MapboxAddress` instances. Besides the regular +address data they expose `getId()`, `getStreetName()`, `getStreetNumber()`, +`getResultType()`, `getFormattedAddress()`, `getNeighborhood()`, +`getMatchCode()` / `getMatchConfidence()` (v6 Smart Address Match) and `getAccuracy()`. + ### Contribute Contributions are very welcome! Send a pull request to the [main repository](https://github.com/geocoder-php/Geocoder) or diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_06551ee827f3b9635793737514ec6b9d368075c4 b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_06551ee827f3b9635793737514ec6b9d368075c4 new file mode 100644 index 000000000..64d3405cb --- /dev/null +++ b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_06551ee827f3b9635793737514ec6b9d368075c4 @@ -0,0 +1 @@ +s:2020:"{"type":"FeatureCollection","features":[{"type":"Feature","id":"dXJuOm1ieGFkcjozOGY3OTg1OC1jNzY0LTQ4ZGUtYTFmMC04NjJjOTM1ZWViMjc","geometry":{"type":"Point","coordinates":[-111.507008,40.6716]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjozOGY3OTg1OC1jNzY0LTQ4ZGUtYTFmMC04NjJjOTM1ZWViMjc","feature_type":"address","full_address":"2595 Lucky John Drive, Park City, Utah 84060, United States","name":"2595 Lucky John Drive","name_preferred":"2595 Lucky John Drive","coordinates":{"longitude":-111.507008,"latitude":40.6716,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":40.6716,"longitude":-111.506529}]},"place_formatted":"Park City, Utah 84060, United States","match_code":{"address_number":"matched","street":"matched","postcode":"matched","place":"matched","region":"matched","locality":"not_applicable","country":"inferred","confidence":"exact"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjozOGY3OTg1OC1jNzY0LTQ4ZGUtYTFmMC04NjJjOTM1ZWViMjc","address_number":"2595","street_name":"Lucky John Drive","name":"2595 Lucky John Drive"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6MzhmNzk4NTgtYzc2NC00OGRlLWExZjAtODYyYzkzNWVlYjI3","name":"Lucky John Drive"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpITktzN0E","name":"Park Meadows"},"postcode":{"mapbox_id":"dXJuOm1ieHBsYzpFUjNPN0E","name":"84060"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpEdjlvN0E","name":"Park City","wikidata_id":"Q482993"},"district":{"mapbox_id":"dXJuOm1ieHBsYzpBVlRHN0E","name":"Summit County","wikidata_id":"Q484563"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpCa1Rz","name":"Utah","wikidata_id":"Q829","region_code":"UT","region_code_full":"US-UT"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"}}}}],"attribution":"NOTICE: © 2026 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_20d856dd6d6ae671b181193de4e52f7fb651fd97 b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_20d856dd6d6ae671b181193de4e52f7fb651fd97 deleted file mode 100644 index 8a7277674..000000000 --- a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_20d856dd6d6ae671b181193de4e52f7fb651fd97 +++ /dev/null @@ -1 +0,0 @@ -s:967:"{"type":"FeatureCollection","query":["princ"],"features":[{"id":"place.4899176537126140","type":"Feature","place_type":["place"],"relevance":1,"properties":{"wikidata":"Q235"},"text_it":"Principato di Monaco","language_it":"it","place_name_it":"Principato di Monaco, Principato di Monaco","text":"Principato di Monaco","language":"it","place_name":"Principato di Monaco, Principato di Monaco","bbox":[7.408977,43.724643,7.440014,43.751895],"center":[7.41974,43.73125],"geometry":{"type":"Point","coordinates":[7.41974,43.73125]},"context":[{"id":"country.3435803554126140","wikidata":"Q235","text_it":"Principato di Monaco","language_it":"it","text":"Principato di Monaco","language":"it"}]}],"attribution":"NOTICE: © 2020 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained. POI(s) provided by Foursquare."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_338a05e2ceeb9a75e55774746945065683127870 b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_338a05e2ceeb9a75e55774746945065683127870 new file mode 100644 index 000000000..4f1aed58f --- /dev/null +++ b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_338a05e2ceeb9a75e55774746945065683127870 @@ -0,0 +1 @@ +s:5571:"{"type":"FeatureCollection","features":[{"type":"Feature","id":"dXJuOm1ieHBsYzpBUVRz","geometry":{"type":"Point","coordinates":[-120.094293,48.025005]},"properties":{"mapbox_id":"dXJuOm1ieHBsYzpBUVRz","feature_type":"region","full_address":"Washington, United States","name":"Washington","name_preferred":"Washington","coordinates":{"longitude":-120.094293,"latitude":48.025005},"place_formatted":"United States","bbox":[-124.861672,45.543637,-116.916072,49.002595],"context":{"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpBUVRz","name":"Washington","region_code":"WA","region_code_full":"US-WA","wikidata_id":"Q1223"}}}},{"type":"Feature","id":"dXJuOm1ieHBsYzpLU3RzN0E","geometry":{"type":"Point","coordinates":[-120.482925,46.59803]},"properties":{"mapbox_id":"dXJuOm1ieHBsYzpLU3RzN0E","feature_type":"neighborhood","full_address":"Washington Park, Yakima, Washington, United States","name":"Washington Park","name_preferred":"Washington Park","coordinates":{"longitude":-120.482925,"latitude":46.59803},"place_formatted":"Yakima, Washington, United States","bbox":[-120.494858,46.592164,-120.481383,46.601423],"context":{"postcode":{"mapbox_id":"dXJuOm1ieHBsYzpFN1B1N0E","name":"98901"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpGZE1JN0E","name":"Yakima","wikidata_id":"Q499203","alternate":{"mapbox_id":"dXJuOm1ieHBsYzpFKy9vN0E","name":"Union Gap"}},"district":{"mapbox_id":"dXJuOm1ieHBsYzpBWVdHN0E","name":"Yakima County","wikidata_id":"Q156629"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpBUVRz","name":"Washington","wikidata_id":"Q1223","region_code":"WA","region_code_full":"US-WA"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpLU3RzN0E","name":"Washington Park"}}}},{"type":"Feature","id":"dXJuOm1ieHBsYzpMcDhzN0E","geometry":{"type":"Point","coordinates":[-123.04517,48.07898]},"properties":{"mapbox_id":"dXJuOm1ieHBsYzpMcDhzN0E","feature_type":"neighborhood","full_address":"Washington Harbor, Sequim, Washington, United States","name":"Washington Harbor","name_preferred":"Washington Harbor","coordinates":{"longitude":-123.04517,"latitude":48.07898},"place_formatted":"Sequim, Washington, United States","bbox":[-123.051913,48.07448,-123.038438,48.083482],"context":{"place":{"mapbox_id":"dXJuOm1ieHBsYzpFY2RJN0E","name":"Sequim","wikidata_id":"Q1514115"},"district":{"mapbox_id":"dXJuOm1ieHBsYzpRTWJz","name":"Clallam County","wikidata_id":"Q156306"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpBUVRz","name":"Washington","wikidata_id":"Q1223","region_code":"WA","region_code_full":"US-WA"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpMcDhzN0E","name":"Washington Harbor"}}}},{"type":"Feature","id":"dXJuOm1ieGFkci1zdHI6ZGNjNjFmZjctZDkwZi00N2U5LTg2OGMtNTMyMWFlZWYyZmY5","geometry":{"type":"Point","coordinates":[-123.381214,48.444226]},"properties":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6ZGNjNjFmZjctZDkwZi00N2U5LTg2OGMtNTMyMWFlZWYyZmY5","feature_type":"street","full_address":"Washington Avenue, Victoria, British Columbia V9A 1P7, Canada","name":"Washington Avenue","name_preferred":"Washington Avenue","coordinates":{"longitude":-123.381214,"latitude":48.444226},"place_formatted":"Victoria, British Columbia V9A 1P7, Canada","context":{"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6ZGNjNjFmZjctZDkwZi00N2U5LTg2OGMtNTMyMWFlZWYyZmY5","name":"Washington Avenue"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpNQ3du","name":"Burnside"},"postcode":{"mapbox_id":"dXJuOm1ieHBsYzpBYVlQYmlj","name":"V9A 1P7"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpCMTVJSnc","name":"Victoria","wikidata_id":"Q2132"},"district":{"mapbox_id":"dXJuOm1ieHBsYzpBcVlu","name":"Capital Regional District","wikidata_id":"Q550219"},"region":{"mapbox_id":"dXJuOm1ieHBsYzo1Q2M","name":"British Columbia","wikidata_id":"Q1973","region_code":"BC","region_code_full":"CA-BC"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJaWM","name":"Canada","wikidata_id":"Q16","country_code":"CA","country_code_alpha_3":"CAN"}}}},{"type":"Feature","id":"address.7607992201284362","geometry":{"type":"Point","coordinates":[-117.039767,46.930253]},"properties":{"mapbox_id":"address.7607992201284362","feature_type":"street","full_address":"Washington Highway 6, Palouse, Washington 99161, United States","name":"Washington Highway 6","name_preferred":"Washington Highway 6","coordinates":{"longitude":-117.039767,"latitude":46.930253},"place_formatted":"Palouse, Washington 99161, United States","context":{"street":{"mapbox_id":"address.7607992201284362","name":"Washington Highway 6"},"postcode":{"mapbox_id":"dXJuOm1ieHBsYzpFOEtPN0E","name":"99161"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpEdkVvN0E","name":"Palouse"},"district":{"mapbox_id":"dXJuOm1ieHBsYzpBWHpHN0E","name":"Whitman County","wikidata_id":"Q484153"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpBUVRz","name":"Washington","wikidata_id":"Q1223","region_code":"WA","region_code_full":"US-WA"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"}}}}],"attribution":"NOTICE: © 2026 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_436e84f1a3531213304e67e5bca76e7550349a2a b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_436e84f1a3531213304e67e5bca76e7550349a2a deleted file mode 100644 index 46df4575c..000000000 --- a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_436e84f1a3531213304e67e5bca76e7550349a2a +++ /dev/null @@ -1 +0,0 @@ -s:331:"{"type":"FeatureCollection","query":["jsajhgsdkfjhsfkjhaldkadjaslgldasd"],"features":[],"attribution":"NOTICE: © 2017 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_5e373c440fdae88e9ec714d42d85489c9986ab7c b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_5e373c440fdae88e9ec714d42d85489c9986ab7c deleted file mode 100644 index 0bcf04c1b..000000000 --- a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_5e373c440fdae88e9ec714d42d85489c9986ab7c +++ /dev/null @@ -1 +0,0 @@ -s:4034:"{"type":"FeatureCollection","query":["wahsington"],"features":[{"id":"address.3196918775570688","type":"Feature","place_type":["address"],"relevance":0.95,"properties":{"accuracy":"street"},"text":"Washington Street","place_name":"Washington Street, Sumas, Washington 98295, United States","center":[-122.2718746,49.0017164],"geometry":{"type":"Point","coordinates":[-122.2718746,49.0017164]},"context":[{"id":"postcode.1103001428069280","text":"98295"},{"id":"place.10997180196070370","wikidata":"Q1510124","text":"Sumas"},{"id":"region.9713796497246050","wikidata":"Q1223","short_code":"US-WA","text":"Washington"},{"id":"country.19678805456372290","wikidata":"Q30","short_code":"us","text":"United States"}]},{"id":"address.1946746068053092","type":"Feature","place_type":["address"],"relevance":0.95,"properties":{"accuracy":"street"},"text":"Washington Avenue","place_name":"Washington Avenue, Victoria, British Columbia V9A 1L5, Canada","matching_text":"Washington Ave E","matching_place_name":"Washington Ave E, Victoria, British Columbia V9A 1L5, Canada","center":[-123.380986,48.4440317],"geometry":{"type":"Point","coordinates":[-123.380986,48.4440317]},"context":[{"id":"neighborhood.11535041776736560","text":"Burnside"},{"id":"postcode.7635888694089700","text":"V9A 1L5"},{"id":"place.7877779600899450","wikidata":"Q2132","text":"Victoria"},{"id":"region.9984400673322020","wikidata":"Q1974","short_code":"CA-BC","text":"British Columbia"},{"id":"country.10278600750587150","wikidata":"Q16","short_code":"ca","text":"Canada"}]},{"id":"address.499813899464900","type":"Feature","place_type":["address"],"relevance":0.95,"properties":{"accuracy":"street"},"text":"Washington Highway 6","place_name":"Washington Highway 6, Palouse, Washington 99161, United States","center":[-117.0397671,46.9302529],"geometry":{"type":"Point","coordinates":[-117.0397671,46.9302529]},"context":[{"id":"postcode.12946567993911350","text":"99161"},{"id":"place.13856508424544360","text":"Palouse"},{"id":"region.9713796497246050","wikidata":"Q1223","short_code":"US-WA","text":"Washington"},{"id":"country.19678805456372290","wikidata":"Q30","short_code":"us","text":"United States"}]},{"id":"address.8713060929842858","type":"Feature","place_type":["address"],"relevance":0.95,"properties":{"accuracy":"street"},"text":"Down River Road","place_name":"Down River Road, Lewiston, Washington 83501, United States","matching_text":"Washington Highway 128","matching_place_name":"Washington Highway 128, Lewiston, Washington 83501, United States","center":[-117.0397913,46.4312588],"geometry":{"type":"Point","coordinates":[-117.0397913,46.4312588]},"context":[{"id":"neighborhood.278688","text":"Transfer"},{"id":"postcode.11154056676467040","text":"83501"},{"id":"place.9371029782746440","wikidata":"Q505539","text":"Lewiston"},{"id":"region.9713796497246050","wikidata":"Q1223","short_code":"US-WA","text":"Washington"},{"id":"country.19678805456372290","wikidata":"Q30","short_code":"us","text":"United States"}]},{"id":"address.8337368900196888","type":"Feature","place_type":["address"],"relevance":0.95,"properties":{"accuracy":"street"},"text":"South Washington Street","place_name":"South Washington Street, Moscow, Idaho 83843, United States","matching_text":"Washington","matching_place_name":"Washington, Moscow, Idaho 83843, United States","center":[-116.9997431,46.7299485],"geometry":{"type":"Point","coordinates":[-116.9997431,46.7299485]},"context":[{"id":"postcode.10023543810932580","text":"83843"},{"id":"place.19031168784618380","wikidata":"Q499927","text":"Moscow"},{"id":"region.9862000087605220","wikidata":"Q1221","short_code":"US-ID","text":"Idaho"},{"id":"country.19678805456372290","wikidata":"Q30","short_code":"us","text":"United States"}]}],"attribution":"NOTICE: © 2020 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained. POI(s) provided by Foursquare."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_5e53474a50aa75267953e6d90b2e3472021efbf2 b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_5e53474a50aa75267953e6d90b2e3472021efbf2 new file mode 100644 index 000000000..3de4f2cbd --- /dev/null +++ b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_5e53474a50aa75267953e6d90b2e3472021efbf2 @@ -0,0 +1 @@ +s:11512:"{"type":"FeatureCollection","features":[{"type":"Feature","id":"dXJuOm1ieGFkcjo3Njg3YjZmNy01YmZkLTQzMjItOGMzOS02OGE1NDhmZWYwM2U","geometry":{"type":"Point","coordinates":[-122.413709,37.7757]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjo3Njg3YjZmNy01YmZkLTQzMjItOGMzOS02OGE1NDhmZWYwM2U","feature_type":"address","full_address":"149 9th Street, San Francisco, California 94103, United States","name":"149 9th Street","name_preferred":"149 9th Street","coordinates":{"longitude":-122.413709,"latitude":37.7757,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":37.775569,"longitude":-122.413874}]},"place_formatted":"San Francisco, California 94103, United States","match_code":{"address_number":"matched","street":"matched","postcode":"matched","place":"matched","region":"matched","locality":"not_applicable","country":"inferred","confidence":"exact"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjo3Njg3YjZmNy01YmZkLTQzMjItOGMzOS02OGE1NDhmZWYwM2U","address_number":"149","street_name":"9th Street","name":"149 9th Street"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6NzY4N2I2ZjctNWJmZC00MzIyLThjMzktNjhhNTQ4ZmVmMDNl","name":"9th Street"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpJOWtzN0E","name":"South of Market","alternate":{"mapbox_id":"dXJuOm1ieHBsYzpCdi9zN0E","name":"Civic Center","wikidata_id":"Q267874"}},"postcode":{"mapbox_id":"dXJuOm1ieHBsYzpFcDlPN0E","name":"94103"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpFVzBJN0E","name":"San Francisco","wikidata_id":"Q62"},"district":{"mapbox_id":"dXJuOm1ieHBsYzpBVG1HN0E","name":"San Francisco County","wikidata_id":"Q62"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpCbVRz","name":"California","wikidata_id":"Q99","region_code":"CA","region_code_full":"US-CA"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"}}}},{"type":"Feature","id":"dXJuOm1ieGFkci1pdHA6ZXlKeElqb2lNVFE1SURsMGFDQlRkQ3dnVTJGdUlFWnlZVzVqYVhOamJ5d2dRMEVnT1RReE1ETWlMQ0owZVhCbGN5STZJbUZrWkhKbGMzTWlMQ0pzYVcxcGRDSTZJalVpTENKcGJtTnNkV1JsWDIxaGRHTm9YMlJsZEdGcGJDSTZJblJ5ZFdVaUxDSnRiMlJsSWpvaVptOXlkMkZ5WkNJc0luWTJkSGx3WlhNaU9pSmhaR1J5WlhOekluMDox","geometry":{"type":"Point","coordinates":[-122.360907,37.932889]},"properties":{"mapbox_id":"dXJuOm1ieGFkci1pdHA6ZXlKeElqb2lNVFE1SURsMGFDQlRkQ3dnVTJGdUlFWnlZVzVqYVhOamJ5d2dRMEVnT1RReE1ETWlMQ0owZVhCbGN5STZJbUZrWkhKbGMzTWlMQ0pzYVcxcGRDSTZJalVpTENKcGJtTnNkV1JsWDIxaGRHTm9YMlJsZEdGcGJDSTZJblJ5ZFdVaUxDSnRiMlJsSWpvaVptOXlkMkZ5WkNJc0luWTJkSGx3WlhNaU9pSmhaR1J5WlhOekluMDox","feature_type":"address","full_address":"149 9th Street, Richmond, California 94804, United States","name":"149 9th Street","name_preferred":"149 9th Street","coordinates":{"longitude":-122.360907,"latitude":37.932889,"accuracy":"interpolated","routable_points":[{"name":"default","latitude":37.932889,"longitude":-122.360907}]},"place_formatted":"Richmond, California 94804, United States","match_code":{"address_number":"plausible","street":"matched","postcode":"unmatched","place":"unmatched","region":"matched","locality":"not_applicable","country":"inferred","confidence":"low"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkci1pdHA6ZXlKeElqb2lNVFE1SURsMGFDQlRkQ3dnVTJGdUlFWnlZVzVqYVhOamJ5d2dRMEVnT1RReE1ETWlMQ0owZVhCbGN5STZJbUZrWkhKbGMzTWlMQ0pzYVcxcGRDSTZJalVpTENKcGJtTnNkV1JsWDIxaGRHTm9YMlJsZEdGcGJDSTZJblJ5ZFdVaUxDSnRiMlJsSWpvaVptOXlkMkZ5WkNJc0luWTJkSGx3WlhNaU9pSmhaR1J5WlhOekluMDox","address_number":"149","street_name":"9th Street","name":"149 9th Street"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHItaXRwOmV5SnhJam9pTVRRNUlEbDBhQ0JUZEN3Z1UyRnVJRVp5WVc1amFYTmpieXdnUTBFZ09UUXhNRE1pTENKMGVYQmxjeUk2SW1Ga1pISmxjM01pTENKc2FXMXBkQ0k2SWpVaUxDSnBibU5zZFdSbFgyMWhkR05vWDJSbGRHRnBiQ0k2SW5SeWRXVWlMQ0p0YjJSbElqb2labTl5ZDJGeVpDSXNJblkyZEhsd1pYTWlPaUpoWkdSeVpYTnpJbjA6MQ","name":"9th Street"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpFdGNNN0E","name":"Iron Triangle","wikidata_id":"Q6072867"},"postcode":{"mapbox_id":"dXJuOm1ieHBsYzpFcjl1N0E","name":"94804"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpGZnhvN0E","name":"Richmond","wikidata_id":"Q495377"},"district":{"mapbox_id":"dXJuOm1ieHBsYzpUa2Jz","name":"Contra Costa County","wikidata_id":"Q108058"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpCbVRz","name":"California","wikidata_id":"Q99","region_code":"CA","region_code_full":"US-CA"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"}}}},{"type":"Feature","id":"dXJuOm1ieGFkci1pdHA6ZXlKeElqb2lNVFE1SURsMGFDQlRkQ3dnVTJGdUlFWnlZVzVqYVhOamJ5d2dRMEVnT1RReE1ETWlMQ0owZVhCbGN5STZJbUZrWkhKbGMzTWlMQ0pzYVcxcGRDSTZJalVpTENKcGJtTnNkV1JsWDIxaGRHTm9YMlJsZEdGcGJDSTZJblJ5ZFdVaUxDSnRiMlJsSWpvaVptOXlkMkZ5WkNJc0luWTJkSGx3WlhNaU9pSmhaR1J5WlhOekluMDoy","geometry":{"type":"Point","coordinates":[-122.239871,38.097967]},"properties":{"mapbox_id":"dXJuOm1ieGFkci1pdHA6ZXlKeElqb2lNVFE1SURsMGFDQlRkQ3dnVTJGdUlFWnlZVzVqYVhOamJ5d2dRMEVnT1RReE1ETWlMQ0owZVhCbGN5STZJbUZrWkhKbGMzTWlMQ0pzYVcxcGRDSTZJalVpTENKcGJtTnNkV1JsWDIxaGRHTm9YMlJsZEdGcGJDSTZJblJ5ZFdVaUxDSnRiMlJsSWpvaVptOXlkMkZ5WkNJc0luWTJkSGx3WlhNaU9pSmhaR1J5WlhOekluMDoy","feature_type":"address","full_address":"149 9th Street, Vallejo, California 94590, United States","name":"149 9th Street","name_preferred":"149 9th Street","coordinates":{"longitude":-122.239871,"latitude":38.097967,"accuracy":"interpolated","routable_points":[{"name":"default","latitude":38.097967,"longitude":-122.239871}]},"place_formatted":"Vallejo, California 94590, United States","match_code":{"address_number":"plausible","street":"matched","postcode":"unmatched","place":"unmatched","region":"matched","locality":"not_applicable","country":"inferred","confidence":"low"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkci1pdHA6ZXlKeElqb2lNVFE1SURsMGFDQlRkQ3dnVTJGdUlFWnlZVzVqYVhOamJ5d2dRMEVnT1RReE1ETWlMQ0owZVhCbGN5STZJbUZrWkhKbGMzTWlMQ0pzYVcxcGRDSTZJalVpTENKcGJtTnNkV1JsWDIxaGRHTm9YMlJsZEdGcGJDSTZJblJ5ZFdVaUxDSnRiMlJsSWpvaVptOXlkMkZ5WkNJc0luWTJkSGx3WlhNaU9pSmhaR1J5WlhOekluMDoy","address_number":"149","street_name":"9th Street","name":"149 9th Street"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHItaXRwOmV5SnhJam9pTVRRNUlEbDBhQ0JUZEN3Z1UyRnVJRVp5WVc1amFYTmpieXdnUTBFZ09UUXhNRE1pTENKMGVYQmxjeUk2SW1Ga1pISmxjM01pTENKc2FXMXBkQ0k2SWpVaUxDSnBibU5zZFdSbFgyMWhkR05vWDJSbGRHRnBiQ0k2SW5SeWRXVWlMQ0p0YjJSbElqb2labTl5ZDJGeVpDSXNJblkyZEhsd1pYTWlPaUpoWkdSeVpYTnpJbjA6Mg","name":"9th Street"},"postcode":{"mapbox_id":"dXJuOm1ieHBsYzpFcmp1N0E","name":"94590"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpGQTFJN0E","name":"Vallejo","wikidata_id":"Q208445"},"district":{"mapbox_id":"dXJuOm1ieHBsYzpBVWtHN0E","name":"Solano County","wikidata_id":"Q108083"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpCbVRz","name":"California","wikidata_id":"Q99","region_code":"CA","region_code_full":"US-CA"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"}}}},{"type":"Feature","id":"dXJuOm1ieGFkci1pdHA6ZXlKeElqb2lNVFE1SURsMGFDQlRkQ3dnVTJGdUlFWnlZVzVqYVhOamJ5d2dRMEVnT1RReE1ETWlMQ0owZVhCbGN5STZJbUZrWkhKbGMzTWlMQ0pzYVcxcGRDSTZJalVpTENKcGJtTnNkV1JsWDIxaGRHTm9YMlJsZEdGcGJDSTZJblJ5ZFdVaUxDSnRiMlJsSWpvaVptOXlkMkZ5WkNJc0luWTJkSGx3WlhNaU9pSmhaR1J5WlhOekluMDoz","geometry":{"type":"Point","coordinates":[-122.266226,37.798081]},"properties":{"mapbox_id":"dXJuOm1ieGFkci1pdHA6ZXlKeElqb2lNVFE1SURsMGFDQlRkQ3dnVTJGdUlFWnlZVzVqYVhOamJ5d2dRMEVnT1RReE1ETWlMQ0owZVhCbGN5STZJbUZrWkhKbGMzTWlMQ0pzYVcxcGRDSTZJalVpTENKcGJtTnNkV1JsWDIxaGRHTm9YMlJsZEdGcGJDSTZJblJ5ZFdVaUxDSnRiMlJsSWpvaVptOXlkMkZ5WkNJc0luWTJkSGx3WlhNaU9pSmhaR1J5WlhOekluMDoz","feature_type":"address","full_address":"149 9th Street, Oakland, California 94607, United States","name":"149 9th Street","name_preferred":"149 9th Street","coordinates":{"longitude":-122.266226,"latitude":37.798081,"accuracy":"interpolated","routable_points":[{"name":"default","latitude":37.798081,"longitude":-122.266226}]},"place_formatted":"Oakland, California 94607, United States","match_code":{"address_number":"plausible","street":"matched","postcode":"unmatched","place":"unmatched","region":"matched","locality":"not_applicable","country":"inferred","confidence":"low"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkci1pdHA6ZXlKeElqb2lNVFE1SURsMGFDQlRkQ3dnVTJGdUlFWnlZVzVqYVhOamJ5d2dRMEVnT1RReE1ETWlMQ0owZVhCbGN5STZJbUZrWkhKbGMzTWlMQ0pzYVcxcGRDSTZJalVpTENKcGJtTnNkV1JsWDIxaGRHTm9YMlJsZEdGcGJDSTZJblJ5ZFdVaUxDSnRiMlJsSWpvaVptOXlkMkZ5WkNJc0luWTJkSGx3WlhNaU9pSmhaR1J5WlhOekluMDoz","address_number":"149","street_name":"9th Street","name":"149 9th Street"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHItaXRwOmV5SnhJam9pTVRRNUlEbDBhQ0JUZEN3Z1UyRnVJRVp5WVc1amFYTmpieXdnUTBFZ09UUXhNRE1pTENKMGVYQmxjeUk2SW1Ga1pISmxjM01pTENKc2FXMXBkQ0k2SWpVaUxDSnBibU5zZFdSbFgyMWhkR05vWDJSbGRHRnBiQ0k2SW5SeWRXVWlMQ0p0YjJSbElqb2labTl5ZDJGeVpDSXNJblkyZEhsd1pYTWlPaUpoWkdSeVpYTnpJbjA6Mw","name":"9th Street"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpCc3RzN0E","name":"Chinatown","wikidata_id":"Q5100154"},"postcode":{"mapbox_id":"dXJuOm1ieHBsYzpFcnF1N0E","name":"94607"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpGZnZvN0E","name":"Oakland","wikidata_id":"Q17042"},"district":{"mapbox_id":"dXJuOm1ieHBsYzpBMGJz","name":"Alameda County","wikidata_id":"Q107146"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpCbVRz","name":"California","wikidata_id":"Q99","region_code":"CA","region_code_full":"US-CA"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"}}}},{"type":"Feature","id":"dXJuOm1ieGFkcjpjNmYwM2Y1MC0zM2MwLTQ1ZmItODYxNi1lOTAwYWM3YjBhM2E","geometry":{"type":"Point","coordinates":[-122.361262,37.929928]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjpjNmYwM2Y1MC0zM2MwLTQ1ZmItODYxNi1lOTAwYWM3YjBhM2E","feature_type":"address","full_address":"149 South 9th Street, Richmond, California 94804, United States","name":"149 South 9th Street","name_preferred":"149 South 9th Street","coordinates":{"longitude":-122.361262,"latitude":37.929928,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":37.929927,"longitude":-122.361032}]},"place_formatted":"Richmond, California 94804, United States","match_code":{"address_number":"matched","street":"unmatched","postcode":"unmatched","place":"unmatched","region":"matched","locality":"not_applicable","country":"inferred","confidence":"low"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjpjNmYwM2Y1MC0zM2MwLTQ1ZmItODYxNi1lOTAwYWM3YjBhM2E","address_number":"149","street_name":"South 9th Street","name":"149 South 9th Street"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6YzZmMDNmNTAtMzNjMC00NWZiLTg2MTYtZTkwMGFjN2IwYTNh","name":"South 9th Street"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpJYndNN0E","name":"Santa Fe"},"postcode":{"mapbox_id":"dXJuOm1ieHBsYzpFcjl1N0E","name":"94804"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpGZnhvN0E","name":"Richmond","wikidata_id":"Q495377"},"district":{"mapbox_id":"dXJuOm1ieHBsYzpUa2Jz","name":"Contra Costa County","wikidata_id":"Q108058"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpCbVRz","name":"California","wikidata_id":"Q99","region_code":"CA","region_code_full":"US-CA"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"}}}}],"attribution":"NOTICE: © 2026 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_61c96e256b090c50a0a9a247c3808fb84b79f55c b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_61c96e256b090c50a0a9a247c3808fb84b79f55c deleted file mode 100644 index 3be44cc8c..000000000 --- a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_61c96e256b090c50a0a9a247c3808fb84b79f55c +++ /dev/null @@ -1 +0,0 @@ -s:5251:"{"type":"FeatureCollection","query":["10","downing","st","london","uk"],"features":[{"id":"address.2844060426507784","type":"Feature","place_type":["address"],"relevance":0.9666666666666666,"properties":{},"text_en":"Downing Street","place_name_en":"10 Downing Street, St. James's, London, SW1A 2AT, United Kingdom","text":"Downing Street","place_name":"10 Downing Street, St. James's, London, SW1A 2AT, United Kingdom","center":[-0.126126,51.503157],"geometry":{"type":"Point","coordinates":[-0.126126,51.503157]},"address":"10","context":[{"id":"postcode.11318645777473120","text_en":"SW1A 2AT","text":"SW1A 2AT"},{"id":"locality.15910638793964150","wikidata":null,"text_en":"St. James's","text":"St. James's"},{"id":"place.8500383470913397","wikidata":"Q84","text_en":"London","language_en":"en","text":"London","language":"en"},{"id":"district.9655698107976620","wikidata":"Q23306","text_en":"Greater London","language_en":"en","text":"Greater London","language":"en"},{"id":"region.3185","short_code":"GB-ENG","wikidata":"Q21","text_en":"England","language_en":"en","text":"England","language":"en"},{"id":"country.358","short_code":"gb","wikidata":"Q145","text_en":"United Kingdom","language_en":"en","text":"United Kingdom","language":"en"}]},{"id":"address.4852922328439012","type":"Feature","place_type":["address"],"relevance":0.3,"properties":{},"text_en":"Rue Downing","place_name_en":"10 Rue Downing, Grande-Digue, New Brunswick, Canada","text":"Rue Downing","place_name":"10 Rue Downing, Grande-Digue, New Brunswick, Canada","matching_text":"Downing Street","matching_place_name":"10 Downing Street, Grande-Digue, New Brunswick, Canada","center":[-64.529877,46.335725],"geometry":{"type":"Point","coordinates":[-64.529877,46.335725]},"address":"10","context":[{"id":"place.18359628229490950","wikidata":"Q2406204","text_en":"Grande-Digue","language_en":"en","text":"Grande-Digue","language":"en"},{"id":"region.218977","short_code":"CA-NB","wikidata":"Q1965","text_en":"New Brunswick","language_en":"en","text":"New Brunswick","language":"en"},{"id":"country.3179","short_code":"ca","wikidata":"Q16","text_en":"Canada","language_en":"en","text":"Canada","language":"en"}]},{"id":"address.2778496043278880","type":"Feature","place_type":["address"],"relevance":0.3,"properties":{},"text_en":"Downing Street","place_name_en":"10 Downing Street, Fredericton, New Brunswick E3A 4M6, Canada","text":"Downing Street","place_name":"10 Downing Street, Fredericton, New Brunswick E3A 4M6, Canada","center":[-66.585237,45.979891],"geometry":{"type":"Point","coordinates":[-66.585237,45.979891]},"address":"10","context":[{"id":"postcode.13170795948825840","text_en":"E3A 4M6","text":"E3A 4M6"},{"id":"place.12649774315478860","wikidata":"Q2138","text_en":"Fredericton","language_en":"en","text":"Fredericton","language":"en"},{"id":"region.218977","short_code":"CA-NB","wikidata":"Q1965","text_en":"New Brunswick","language_en":"en","text":"New Brunswick","language":"en"},{"id":"country.3179","short_code":"ca","wikidata":"Q16","text_en":"Canada","language_en":"en","text":"Canada","language":"en"}]},{"id":"address.2232726546138078","type":"Feature","place_type":["address"],"relevance":0.3,"properties":{},"text_en":"Downing Street","place_name_en":"10 Downing Street, Hamilton, Ontario L0R 0A0, Canada","text":"Downing Street","place_name":"10 Downing Street, Hamilton, Ontario L0R 0A0, Canada","center":[-79.810597,43.116989],"geometry":{"type":"Point","coordinates":[-79.810597,43.116989],"interpolated":true},"address":"10","context":[{"id":"postcode.19608462426414870","text_en":"L0R 0A0","text":"L0R 0A0"},{"id":"place.14129018412865700","wikidata":"Q133116","text_en":"Hamilton","language_en":"en","text":"Hamilton","language":"en"},{"id":"region.3721","short_code":"CA-ON","wikidata":"Q1904","text_en":"Ontario","language_en":"en","text":"Ontario","language":"en"},{"id":"country.3179","short_code":"ca","wikidata":"Q16","text_en":"Canada","language_en":"en","text":"Canada","language":"en"}]},{"id":"address.4850220298517218","type":"Feature","place_type":["address"],"relevance":0.3,"properties":{},"text_en":"Downing Street","place_name_en":"10 Downing Street, Halifax, Nova Scotia B3M 2G3, Canada","text":"Downing Street","place_name":"10 Downing Street, Halifax, Nova Scotia B3M 2G3, Canada","center":[-63.669482,44.685001],"geometry":{"type":"Point","coordinates":[-63.669482,44.685001],"interpolated":true},"address":"10","context":[{"id":"neighborhood.11574814505474780","text_en":"Rockingham","text":"Rockingham"},{"id":"postcode.13399737010185770","text_en":"B3M 2G3","text":"B3M 2G3"},{"id":"place.17790518833069190","wikidata":"Q2141","text_en":"Halifax","language_en":"en","text":"Halifax","language":"en"},{"id":"region.218945","short_code":"CA-NS","wikidata":"Q1952","text_en":"Nova Scotia","language_en":"en","text":"Nova Scotia","language":"en"},{"id":"country.3179","short_code":"ca","wikidata":"Q16","text_en":"Canada","language_en":"en","text":"Canada","language":"en"}]}],"attribution":"NOTICE: © 2017 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_7b8b47952b295102acd4cb1bc0a9536c26c7bc9b b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_7b8b47952b295102acd4cb1bc0a9536c26c7bc9b new file mode 100644 index 000000000..5fac39b0b --- /dev/null +++ b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_7b8b47952b295102acd4cb1bc0a9536c26c7bc9b @@ -0,0 +1 @@ +s:6626:"{"type":"FeatureCollection","features":[{"type":"Feature","id":"dXJuOm1ieGFkcjpjM2M2NWFjNC1hMTExLTQ1NDItOGIxNi05ZDg1Y2I4YjYzMDc","geometry":{"type":"Point","coordinates":[2.388886,48.863134]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjpjM2M2NWFjNC1hMTExLTQ1NDItOGIxNi05ZDg1Y2I4YjYzMDc","feature_type":"address","full_address":"12 Avenue Gambetta, 75020 Paris, France","name":"12 Avenue Gambetta","name_preferred":"12 Avenue Gambetta","coordinates":{"longitude":2.388886,"latitude":48.863134,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":48.863236,"longitude":2.388829}]},"place_formatted":"75020 Paris, France","context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjpjM2M2NWFjNC1hMTExLTQ1NDItOGIxNi05ZDg1Y2I4YjYzMDc","address_number":"12","street_name":"Avenue Gambetta","name":"12 Avenue Gambetta"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6YzNjNjVhYzQtYTExMS00NTQyLThiMTYtOWQ4NWNiOGI2MzA3","name":"Avenue Gambetta"},"postcode":{"mapbox_id":"postcode.3248070486542928","name":"75020"},"locality":{"mapbox_id":"dXJuOm1ieHBsYzpDRXBO","name":"20th arrondissement","wikidata_id":"Q210720"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpEYVJO","name":"Paris","wikidata_id":"Q90","short_code":"FR-75"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpFc1JO","name":"Île-de-France","wikidata_id":"Q13917","region_code":"IDF","region_code_full":"FR-IDF"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJazA","name":"France","wikidata_id":"Q142","country_code":"FR","country_code_alpha_3":"FRA"}}}},{"type":"Feature","id":"dXJuOm1ieGFkcjozZjgzZmVjMi02N2UzLTRiMzctOGY4Yi01ZDVlNTNkMDgzODQ","geometry":{"type":"Point","coordinates":[2.388742,48.863306]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjozZjgzZmVjMi02N2UzLTRiMzctOGY4Yi01ZDVlNTNkMDgzODQ","feature_type":"address","full_address":"1 Avenue Gambetta, 75020 Paris, France","name":"1 Avenue Gambetta","name_preferred":"1 Avenue Gambetta","coordinates":{"longitude":2.388742,"latitude":48.863306,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":48.863226,"longitude":2.388787}]},"place_formatted":"75020 Paris, France","context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjozZjgzZmVjMi02N2UzLTRiMzctOGY4Yi01ZDVlNTNkMDgzODQ","address_number":"1","street_name":"Avenue Gambetta","name":"1 Avenue Gambetta"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6M2Y4M2ZlYzItNjdlMy00YjM3LThmOGItNWQ1ZTUzZDA4Mzg0","name":"Avenue Gambetta"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpCU25NVFE","name":"Menilmontant","alternate":{"mapbox_id":"dXJuOm1ieHBsYzpCU3NNVFE","name":"Père-Lachaise","wikidata_id":"Q3413289"}},"postcode":{"mapbox_id":"postcode.3248070486542928","name":"75020"},"locality":{"mapbox_id":"dXJuOm1ieHBsYzpDRXBO","name":"20th arrondissement","wikidata_id":"Q210720"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpEYVJO","name":"Paris","wikidata_id":"Q90","short_code":"FR-75"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpFc1JO","name":"Île-de-France","wikidata_id":"Q13917","region_code":"IDF","region_code_full":"FR-IDF"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJazA","name":"France","wikidata_id":"Q142","country_code":"FR","country_code_alpha_3":"FRA"}}}},{"type":"Feature","id":"dXJuOm1ieGFkcjphNmFhMTZmOS1jZWE3LTQ0ZTAtOGYwYS1lMjc1NDVhMTdiN2I","geometry":{"type":"Point","coordinates":[2.38854,48.863297]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjphNmFhMTZmOS1jZWE3LTQ0ZTAtOGYwYS1lMjc1NDVhMTdiN2I","feature_type":"address","full_address":"13 Place Auguste Métivier, 75020 Paris, France","name":"13 Place Auguste Métivier","name_preferred":"13 Place Auguste Métivier","coordinates":{"longitude":2.38854,"latitude":48.863297,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":48.863242,"longitude":2.388245}]},"place_formatted":"75020 Paris, France","context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjphNmFhMTZmOS1jZWE3LTQ0ZTAtOGYwYS1lMjc1NDVhMTdiN2I","address_number":"13","street_name":"Place Auguste Métivier","name":"13 Place Auguste Métivier"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6YTZhYTE2ZjktY2VhNy00NGUwLThmMGEtZTI3NTQ1YTE3Yjdi","name":"Place Auguste Métivier"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpCU25NVFE","name":"Menilmontant","alternate":{"mapbox_id":"dXJuOm1ieHBsYzpCU3NNVFE","name":"Père-Lachaise","wikidata_id":"Q3413289"}},"postcode":{"mapbox_id":"postcode.8604773807997748","name":"75020"},"locality":{"mapbox_id":"dXJuOm1ieHBsYzpDRXBO","name":"20th arrondissement","wikidata_id":"Q210720"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpEYVJO","name":"Paris","wikidata_id":"Q90","short_code":"FR-75"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpFc1JO","name":"Île-de-France","wikidata_id":"Q13917","region_code":"IDF","region_code_full":"FR-IDF"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJazA","name":"France","wikidata_id":"Q142","country_code":"FR","country_code_alpha_3":"FRA"}}}},{"type":"Feature","id":"dXJuOm1ieGFkcjpjYzVkYzQ3MS1lNGJiLTRiOTItYTU1MC1lZTg4ZTFjOWUzMmE","geometry":{"type":"Point","coordinates":[2.388546,48.863359]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjpjYzVkYzQ3MS1lNGJiLTRiOTItYTU1MC1lZTg4ZTFjOWUzMmE","feature_type":"address","full_address":"2 Rue Des Amandiers, 75020 Paris, France","name":"2 Rue Des Amandiers","name_preferred":"2 Rue Des Amandiers","coordinates":{"longitude":2.388546,"latitude":48.863359,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":48.863389,"longitude":2.388492}]},"place_formatted":"75020 Paris, France","context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjpjYzVkYzQ3MS1lNGJiLTRiOTItYTU1MC1lZTg4ZTFjOWUzMmE","address_number":"2","street_name":"Rue Des Amandiers","name":"2 Rue Des Amandiers"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6Y2M1ZGM0NzEtZTRiYi00YjkyLWE1NTAtZWU4OGUxYzllMzJh","name":"Rue Des Amandiers"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpCU25NVFE","name":"Menilmontant","alternate":{"mapbox_id":"dXJuOm1ieHBsYzpCU3NNVFE","name":"Père-Lachaise","wikidata_id":"Q3413289"}},"postcode":{"mapbox_id":"postcode.5685635047513804","name":"75020"},"locality":{"mapbox_id":"dXJuOm1ieHBsYzpDRXBO","name":"20th arrondissement","wikidata_id":"Q210720"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpEYVJO","name":"Paris","wikidata_id":"Q90","short_code":"FR-75"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpFc1JO","name":"Île-de-France","wikidata_id":"Q13917","region_code":"IDF","region_code_full":"FR-IDF"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJazA","name":"France","wikidata_id":"Q142","country_code":"FR","country_code_alpha_3":"FRA"}}}}],"attribution":"NOTICE: © 2026 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_9e3e5555aca6117bf14dbd86c37ed23cbece335d b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_9e3e5555aca6117bf14dbd86c37ed23cbece335d new file mode 100644 index 000000000..3c6af5dd0 --- /dev/null +++ b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_9e3e5555aca6117bf14dbd86c37ed23cbece335d @@ -0,0 +1 @@ +s:1325:"{"type":"FeatureCollection","features":[{"type":"Feature","id":"address.858151070459380","geometry":{"type":"Point","coordinates":[-122.949686,47.629081]},"properties":{"mapbox_id":"address.858151070459380","feature_type":"street","full_address":"Yashi Road, Brinnon, Washington 98320, United States","name":"Yashi Road","name_preferred":"Yashi Road","coordinates":{"longitude":-122.949686,"latitude":47.629081},"place_formatted":"Brinnon, Washington 98320, United States","context":{"street":{"mapbox_id":"address.858151070459380","name":"Yashi Road"},"postcode":{"mapbox_id":"dXJuOm1ieHBsYzpFNDVPN0E","name":"98320"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpBa2JJN0E","name":"Brinnon","wikidata_id":"Q612805"},"district":{"mapbox_id":"dXJuOm1ieHBsYzpyMGJz","name":"Jefferson County","wikidata_id":"Q384737"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpBUVRz","name":"Washington","wikidata_id":"Q1223","region_code":"WA","region_code_full":"US-WA"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"}}}}],"attribution":"NOTICE: © 2026 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_bbef3c8c03f148c252a89f0a36e1872e67c9a761 b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_bbef3c8c03f148c252a89f0a36e1872e67c9a761 deleted file mode 100644 index 816e76110..000000000 --- a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_bbef3c8c03f148c252a89f0a36e1872e67c9a761 +++ /dev/null @@ -1 +0,0 @@ -s:2309:"{"type":"FeatureCollection","query":[2.388911,48.863151],"features":[{"id":"address.1085979616","type":"Feature","place_type":["address"],"relevance":1,"properties":{},"text":"Avenue Gambetta","place_name":"8 Avenue Gambetta, 75020 Paris, France","center":[2.388781,48.863109],"geometry":{"type":"Point","coordinates":[2.388781,48.863109]},"address":"8","context":[{"id":"postcode.11865225190696130","text":"75020"},{"id":"place.228080","short_code":"FR-75","wikidata":"Q90","text":"Paris"},{"id":"country.3148","wikidata":"Q142","short_code":"fr","text":"France"}]},{"id":"address.1085979616","type":"Feature","place_type":["address"],"relevance":1,"properties":{},"text":"Avenue Gambetta","place_name":"10 Avenue Gambetta, 75020 Paris, France","center":[2.389073,48.863181],"geometry":{"type":"Point","coordinates":[2.389073,48.863181]},"address":"10","context":[{"id":"postcode.11865225190696130","text":"75020"},{"id":"place.228080","short_code":"FR-75","wikidata":"Q90","text":"Paris"},{"id":"country.3148","wikidata":"Q142","short_code":"fr","text":"France"}]},{"id":"address.1085979616","type":"Feature","place_type":["address"],"relevance":1,"properties":{},"text":"Avenue Gambetta","place_name":"1 Avenue Gambetta, 75020 Paris, France","center":[2.388736,48.863305],"geometry":{"type":"Point","coordinates":[2.388736,48.863305]},"address":"1","context":[{"id":"postcode.11865225190696130","text":"75020"},{"id":"place.228080","short_code":"FR-75","wikidata":"Q90","text":"Paris"},{"id":"country.3148","wikidata":"Q142","short_code":"fr","text":"France"}]},{"id":"address.1085979616","type":"Feature","place_type":["address"],"relevance":1,"properties":{},"text":"Avenue Gambetta","place_name":"12 Avenue Gambetta, 75020 Paris, France","center":[2.389194,48.863214],"geometry":{"type":"Point","coordinates":[2.389194,48.863214]},"address":"12","context":[{"id":"postcode.11865225190696130","text":"75020"},{"id":"place.228080","short_code":"FR-75","wikidata":"Q90","text":"Paris"},{"id":"country.3148","wikidata":"Q142","short_code":"fr","text":"France"}]}],"attribution":"NOTICE: © 2017 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_c2fd1618bb5f29f1998244add688fc1668ec2416 b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_c2fd1618bb5f29f1998244add688fc1668ec2416 deleted file mode 100644 index dfa6e6864..000000000 --- a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_c2fd1618bb5f29f1998244add688fc1668ec2416 +++ /dev/null @@ -1 +0,0 @@ -s:530:"{"type":"FeatureCollection","query":[0,0],"features":[{"id":"address.262530399228832","type":"Feature","place_type":["address"],"relevance":1,"properties":{},"text":"South St","place_name":"206 South St, ","center":[0,0],"geometry":{"type":"Point","coordinates":[0,0]},"address":"206"}],"attribution":"NOTICE: © 2017 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_c97f9d6eb99ff44a7e13d0dc8eb54c3fd8f6971b b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_c97f9d6eb99ff44a7e13d0dc8eb54c3fd8f6971b deleted file mode 100644 index fa170aee9..000000000 --- a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_c97f9d6eb99ff44a7e13d0dc8eb54c3fd8f6971b +++ /dev/null @@ -1 +0,0 @@ -s:3470:"{"type":"FeatureCollection","query":[-77.036991,38.900206],"features":[{"id":"address.717957002859378","type":"Feature","place_type":["address"],"relevance":1,"properties":{},"text_en":"H Street Northwest","place_name_en":"1615 H Street Northwest, Washington, District of Columbia 20006, United States","text":"H Street Northwest","place_name":"1615 H Street Northwest, Washington, District of Columbia 20006, United States","center":[-77.037527,38.900583],"geometry":{"type":"Point","coordinates":[-77.037527,38.900583]},"address":"1615","context":[{"id":"neighborhood.291451","text_en":"Downtown","text":"Downtown"},{"id":"postcode.10186570277652840","text_en":"20006","text":"20006"},{"id":"place.7673410831246050","wikidata":"Q61","text_en":"Washington","language_en":"en","text":"Washington","language":"en"},{"id":"region.3403","short_code":"US-DC","wikidata":"Q61","text_en":"District of Columbia","language_en":"en","text":"District of Columbia","language":"en"},{"id":"country.3145","wikidata":"Q30","short_code":"us","text_en":"United States","language_en":"en","text":"United States","language":"en"}]},{"id":"address.8110859437053248","type":"Feature","place_type":["address"],"relevance":1,"properties":{},"text_en":"16th Street Northwest","place_name_en":"800 16th Street Northwest, Washington, District of Columbia 20006, United States","text":"16th Street Northwest","place_name":"800 16th Street Northwest, Washington, District of Columbia 20006, United States","center":[-77.036959,38.900533],"geometry":{"type":"Point","coordinates":[-77.036959,38.900533]},"address":"800","context":[{"id":"neighborhood.291451","text_en":"Downtown","text":"Downtown"},{"id":"postcode.10186570277652840","text_en":"20006","text":"20006"},{"id":"place.7673410831246050","wikidata":"Q61","text_en":"Washington","language_en":"en","text":"Washington","language":"en"},{"id":"region.3403","short_code":"US-DC","wikidata":"Q61","text_en":"District of Columbia","language_en":"en","text":"District of Columbia","language":"en"},{"id":"country.3145","wikidata":"Q30","short_code":"us","text_en":"United States","language_en":"en","text":"United States","language":"en"}]},{"id":"address.45528587931684","type":"Feature","place_type":["address"],"relevance":1,"properties":{},"text_en":"Jackson Place Northwest","place_name_en":"748 Jackson Place Northwest, Washington, District of Columbia 20062, United States","text":"Jackson Place Northwest","place_name":"748 Jackson Place Northwest, Washington, District of Columbia 20062, United States","center":[-77.038199,38.900034],"geometry":{"type":"Point","coordinates":[-77.038199,38.900034]},"address":"748","context":[{"id":"neighborhood.291451","text_en":"Downtown","text":"Downtown"},{"id":"postcode.10886799999681030","text_en":"20062","text":"20062"},{"id":"place.7673410831246050","wikidata":"Q61","text_en":"Washington","language_en":"en","text":"Washington","language":"en"},{"id":"region.3403","short_code":"US-DC","wikidata":"Q61","text_en":"District of Columbia","language_en":"en","text":"District of Columbia","language":"en"},{"id":"country.3145","wikidata":"Q30","short_code":"us","text_en":"United States","language_en":"en","text":"United States","language":"en"}]}],"attribution":"NOTICE: © 2017 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_d72ced1f54dff6389745d11b002a772451804618 b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_d72ced1f54dff6389745d11b002a772451804618 deleted file mode 100644 index 6f03b3ead..000000000 --- a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_d72ced1f54dff6389745d11b002a772451804618 +++ /dev/null @@ -1 +0,0 @@ -s:339:"{"type":"FeatureCollection","query":["wahsington"],"features":[],"attribution":"NOTICE: © 2020 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained. POI(s) provided by Foursquare."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_d73a2016dbd70a152bffadf02f4f04d7e4d6a0b7 b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_d73a2016dbd70a152bffadf02f4f04d7e4d6a0b7 new file mode 100644 index 000000000..1cbcbe74c --- /dev/null +++ b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_d73a2016dbd70a152bffadf02f4f04d7e4d6a0b7 @@ -0,0 +1 @@ +s:1516:"{"type":"FeatureCollection","features":[{"type":"Feature","id":"dXJuOm1ieHBsYzpoMmhQ","geometry":{"type":"Point","coordinates":[-0.83069,51.724422]},"properties":{"mapbox_id":"dXJuOm1ieHBsYzpoMmhQ","feature_type":"place","full_address":"Princes Risborough, Buckinghamshire, Inghilterra, Regno Unito","name":"Princes Risborough","name_preferred":"Princes Risborough","coordinates":{"longitude":-0.83069,"latitude":51.724422},"place_formatted":"Buckinghamshire, Inghilterra, Regno Unito","bbox":[-0.912802,51.680448,-0.770371,51.756426],"context":{"district":{"mapbox_id":"dXJuOm1ieHBsYzpBcVpQ","name":"Buckinghamshire","wikidata_id":"Q21694636","translations":{"it":{"language":"fr","name":"Buckinghamshire"}}},"region":{"mapbox_id":"dXJuOm1ieHBsYzpKRTg","name":"Inghilterra","wikidata_id":"Q21","region_code":"ENG","region_code_full":"GB-ENG","translations":{"it":{"language":"it","name":"Inghilterra"}}},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJazg","name":"Regno Unito","wikidata_id":"Q145","country_code":"GB","country_code_alpha_3":"GBR","translations":{"it":{"language":"it","name":"Regno Unito"}}},"place":{"mapbox_id":"dXJuOm1ieHBsYzpoMmhQ","name":"Princes Risborough","translations":{"it":{"language":"it","name":"Princes Risborough"}},"wikidata_id":"Q2568545"}}}}],"attribution":"NOTICE: © 2026 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_eb032a2644f68b47a4076ea601305eaf8ba72a31 b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_eb032a2644f68b47a4076ea601305eaf8ba72a31 deleted file mode 100644 index 6dde633d7..000000000 --- a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_eb032a2644f68b47a4076ea601305eaf8ba72a31 +++ /dev/null @@ -1 +0,0 @@ -s:3926:"{"type":"FeatureCollection","query":["149","9th","st","san","francisco","ca","94103"],"features":[{"id":"address.3071152063251042","type":"Feature","place_type":["address"],"relevance":0.875,"properties":{},"text":"9th Street","place_name":"149 9th Street, San Francisco, California 94103, United States","center":[-122.41362,37.77572],"geometry":{"type":"Point","coordinates":[-122.41362,37.77572]},"address":"149","context":[{"id":"neighborhood.293577","text":"South of Market"},{"id":"postcode.13892342768265050","text":"94103"},{"id":"place.15734669613361910","wikidata":"Q62","text":"San Francisco"},{"id":"region.3591","short_code":"US-CA","wikidata":"Q99","text":"California"},{"id":"country.3145","short_code":"us","wikidata":"Q30","text":"United States"}]},{"id":"address.2787669249858206","type":"Feature","place_type":["address"],"relevance":0.25,"properties":{},"text":"9th Street","place_name":"149 9th Street, Greenport, New York 11944, United States","center":[-72.370864,41.099811],"geometry":{"type":"Point","coordinates":[-72.370864,41.099811],"interpolated":true},"address":"149","context":[{"id":"neighborhood.278108","text":"Stirling"},{"id":"postcode.5709909550190040","text":"11944"},{"id":"place.10410131002602850","wikidata":"Q94652","text":"Greenport"},{"id":"region.3866","short_code":"US-NY","wikidata":"Q1384","text":"New York"},{"id":"country.3145","short_code":"us","wikidata":"Q30","text":"United States"}]},{"id":"address.1299539993830358","type":"Feature","place_type":["address"],"relevance":0.25,"properties":{},"text":"East 9th Street","place_name":"149 East 9th Street, Erie, Pennsylvania 16503, United States","matching_text":"9th Street","matching_place_name":"149 9th Street, Erie, Pennsylvania 16503, United States","center":[-80.069022,42.131794],"geometry":{"type":"Point","coordinates":[-80.069022,42.131794],"interpolated":true},"address":"149","context":[{"id":"postcode.11103612841590770","text":"16503"},{"id":"place.10004124579967850","wikidata":"Q234061","text":"Erie"},{"id":"region.3337","short_code":"US-PA","wikidata":"Q1400","text":"Pennsylvania"},{"id":"country.3145","short_code":"us","wikidata":"Q30","text":"United States"}]},{"id":"address.7335913800968804","type":"Feature","place_type":["address"],"relevance":0.25,"properties":{},"text":"West 9th Street","place_name":"149 West 9th Street, Erie, Pennsylvania 16501, United States","matching_text":"9th Street","matching_place_name":"149 9th Street, Erie, Pennsylvania 16501, United States","center":[-80.086582,42.125322],"geometry":{"type":"Point","coordinates":[-80.086582,42.125322],"interpolated":true},"address":"149","context":[{"id":"neighborhood.270119","text":"Dock Junction"},{"id":"postcode.11009455618026990","text":"16501"},{"id":"place.10004124579967850","wikidata":"Q234061","text":"Erie"},{"id":"region.3337","short_code":"US-PA","wikidata":"Q1400","text":"Pennsylvania"},{"id":"country.3145","short_code":"us","wikidata":"Q30","text":"United States"}]},{"id":"address.8377599986498868","type":"Feature","place_type":["address"],"relevance":0.25,"properties":{},"text":"Ninth Street","place_name":"149 Ninth Street, Nashua, New Hampshire 03060, United States","center":[-71.481591,42.753499],"geometry":{"type":"Point","coordinates":[-71.481591,42.753499],"interpolated":true,"omitted":true},"address":"149","context":[{"id":"neighborhood.287645","text":"Downtown"},{"id":"postcode.17825302421333580","text":"03060"},{"id":"place.6791107671778960","wikidata":"Q48274","text":"Nashua"},{"id":"region.211885","short_code":"US-NH","wikidata":"Q759","text":"New Hampshire"},{"id":"country.3145","short_code":"us","wikidata":"Q30","text":"United States"}]}],"attribution":"NOTICE: © 2017 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_f212d067263ee5a25cce4e79ee710e3a4af3fc5e b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_f212d067263ee5a25cce4e79ee710e3a4af3fc5e deleted file mode 100644 index c88d497a0..000000000 --- a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_f212d067263ee5a25cce4e79ee710e3a4af3fc5e +++ /dev/null @@ -1 +0,0 @@ -s:4352:"{"type":"FeatureCollection","query":["116th","st","broadway","new","york","ny","10027","united","states"],"features":[{"id":"address.2431617896783536","type":"Feature","place_type":["address"],"relevance":0.6666666666666666,"properties":{},"text":"116th Street","place_name":"116th Street, Queens, New York, New York 11356, United States","center":[-73.851157,40.786596],"geometry":{"type":"Point","coordinates":[-73.851157,40.786596]},"context":[{"id":"neighborhood.2104291","text":"College Point"},{"id":"locality.6306827455006740","wikidata":"Q18424","text":"Queens"},{"id":"postcode.11196253910231740","text":"11356"},{"id":"place.15278078705964500","wikidata":"Q60","text":"New York"},{"id":"region.3866","short_code":"US-NY","wikidata":"Q1384","text":"New York"},{"id":"country.3145","short_code":"us","wikidata":"Q30","text":"United States"}]},{"id":"address.4427827672964774","type":"Feature","place_type":["address"],"relevance":0.5833333333333333,"properties":{},"text":"East 116th Street","place_name":"East 116th Street, Manhattan, New York, New York 10035, United States","matching_text":"116th Street","matching_place_name":"116th Street, Manhattan, New York, New York 10035, United States","center":[-73.938724,40.797571],"geometry":{"type":"Point","coordinates":[-73.938724,40.797571]},"context":[{"id":"neighborhood.2100588","text":"East Harlem"},{"id":"locality.12696928000137850","wikidata":"Q11299","text":"Manhattan"},{"id":"postcode.11255904502577980","text":"10035"},{"id":"place.15278078705964500","wikidata":"Q60","text":"New York"},{"id":"region.3866","short_code":"US-NY","wikidata":"Q1384","text":"New York"},{"id":"country.3145","short_code":"us","wikidata":"Q30","text":"United States"}]},{"id":"address.2741753979749180","type":"Feature","place_type":["address"],"relevance":0.5833333333333333,"properties":{},"text":"West 116th Street","place_name":"West 116th Street, Manhattan, New York, New York 10026, United States","matching_text":"116th Street","matching_place_name":"116th Street, Manhattan, New York, New York 10026, United States","center":[-73.956278,40.804979],"geometry":{"type":"Point","coordinates":[-73.956278,40.804979]},"context":[{"id":"neighborhood.2102863","text":"Harlem"},{"id":"locality.12696928000137850","wikidata":"Q11299","text":"Manhattan"},{"id":"postcode.13768369279157190","text":"10026"},{"id":"place.15278078705964500","wikidata":"Q60","text":"New York"},{"id":"region.3866","short_code":"US-NY","wikidata":"Q1384","text":"New York"},{"id":"country.3145","short_code":"us","wikidata":"Q30","text":"United States"}]},{"id":"address.2201553461107342","type":"Feature","place_type":["address"],"relevance":0.5833333333333333,"properties":{},"text":"116th Street","place_name":"116th Street, Queens, New York, New York 11419, United States","matching_place_name":"116th Street, Queens, NY, New York 11419, United States","center":[-73.827854,40.687554],"geometry":{"type":"Point","coordinates":[-73.827854,40.687554]},"context":[{"id":"neighborhood.297765","text":"Richmond Hill"},{"id":"locality.6306827455006740","wikidata":"Q18424","text":"Queens"},{"id":"postcode.453327290579340","text":"11419"},{"id":"place.15278078705964500","wikidata":"Q60","text":"New York"},{"id":"region.3866","short_code":"US-NY","wikidata":"Q1384","text":"New York"},{"id":"country.3145","short_code":"us","wikidata":"Q30","text":"United States"}]},{"id":"address.3561887178367494","type":"Feature","place_type":["address"],"relevance":0.42857142857142855,"properties":{},"text":"South West Street","place_name":"South West Street, Mount Vernon, New York 10550, United States","matching_text":"St","matching_place_name":"St, Mount Vernon, New York 10550, United States","center":[-73.850347,40.912431],"geometry":{"type":"Point","coordinates":[-73.850347,40.912431]},"context":[{"id":"postcode.15487211573224510","text":"10550"},{"id":"place.9087657435224330","wikidata":"Q835831","text":"Mount Vernon"},{"id":"region.3866","short_code":"US-NY","wikidata":"Q1384","text":"New York"},{"id":"country.3145","short_code":"us","wikidata":"Q30","text":"United States"}]}],"attribution":"NOTICE: © 2017 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_fee388586d54f2150af453c6e033e452d85e46a8 b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_fee388586d54f2150af453c6e033e452d85e46a8 new file mode 100644 index 000000000..383d14da5 --- /dev/null +++ b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_fee388586d54f2150af453c6e033e452d85e46a8 @@ -0,0 +1 @@ +s:3898:"{"type":"FeatureCollection","features":[{"type":"Feature","id":"dXJuOm1ieGFkcjoxMDg4MTIxNi1mZTUwLTQyM2QtOWNjYS00ODQxYjNjMmFkNTA","geometry":{"type":"Point","coordinates":[-77.036574,38.897684]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjoxMDg4MTIxNi1mZTUwLTQyM2QtOWNjYS00ODQxYjNjMmFkNTA","feature_type":"address","full_address":"1600 Pennsylvania Avenue Northwest, Washington, District of Columbia 20500, United States","name":"1600 Pennsylvania Avenue Northwest","name_preferred":"1600 Pennsylvania Avenue Northwest","coordinates":{"longitude":-77.036574,"latitude":38.897684,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":38.897876,"longitude":-77.036544}]},"place_formatted":"Washington, District of Columbia 20500, United States","match_code":{"address_number":"matched","street":"matched","postcode":"matched","place":"matched","region":"matched","locality":"not_applicable","country":"inferred","confidence":"exact"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjoxMDg4MTIxNi1mZTUwLTQyM2QtOWNjYS00ODQxYjNjMmFkNTA","address_number":"1600","street_name":"Pennsylvania Avenue Northwest","name":"1600 Pennsylvania Avenue Northwest"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6MTA4ODEyMTYtZmU1MC00MjNkLTljY2EtNDg0MWIzYzJhZDUw","name":"Pennsylvania Avenue Northwest"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpHYUVzN0E","name":"National Mall"},"postcode":{"mapbox_id":"dXJuOm1ieHBsYzpHVnFPN0E","name":"20500"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpGSmlvN0E","name":"Washington","wikidata_id":"Q61"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpCUVRz","name":"District of Columbia","wikidata_id":"Q3551781","region_code":"DC","region_code_full":"US-DC"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"}}}},{"type":"Feature","id":"dXJuOm1ieGFkcjpmOTYwMzc4OC01MWE4LTQ3NzQtOWVhMi1lM2ZkZGY0NWQ0OWI","geometry":{"type":"Point","coordinates":[-76.981937,38.87922]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjpmOTYwMzc4OC01MWE4LTQ3NzQtOWVhMi1lM2ZkZGY0NWQ0OWI","feature_type":"address","full_address":"1600 Pennsylvania Avenue Southeast, Washington, District of Columbia 20003, United States","name":"1600 Pennsylvania Avenue Southeast","name_preferred":"1600 Pennsylvania Avenue Southeast","coordinates":{"longitude":-76.981937,"latitude":38.87922,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":38.879037,"longitude":-76.98206}]},"place_formatted":"Washington, District of Columbia 20003, United States","match_code":{"address_number":"matched","street":"unmatched","postcode":"unmatched","place":"matched","region":"matched","locality":"not_applicable","country":"inferred","confidence":"medium"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjpmOTYwMzc4OC01MWE4LTQ3NzQtOWVhMi1lM2ZkZGY0NWQ0OWI","address_number":"1600","street_name":"Pennsylvania Avenue Southeast","name":"1600 Pennsylvania Avenue Southeast"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6Zjk2MDM3ODgtNTFhOC00Nzc0LTllYTItZTNmZGRmNDVkNDli","name":"Pennsylvania Avenue Southeast"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpCVnVzN0E","name":"Capitol Hill","alternate":{"mapbox_id":"dXJuOm1ieHBsYzpFVzhzN0E","name":"Hill East"}},"postcode":{"mapbox_id":"dXJuOm1ieHBsYzpBNTZ1N0E","name":"20003"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpGSmlvN0E","name":"Washington","wikidata_id":"Q61"},"region":{"mapbox_id":"dXJuOm1ieHBsYzpCUVRz","name":"District of Columbia","wikidata_id":"Q3551781","region_code":"DC","region_code_full":"US-DC"},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA"}}}}],"attribution":"NOTICE: © 2026 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file From 86873e8f1e298ff8d020b6ca91249a61db0e6da4 Mon Sep 17 00:00:00 2001 From: David Barratt Date: Fri, 28 Aug 2026 21:29:42 +0000 Subject: [PATCH 10/13] test(mapbox): record v6 fixtures for the provider integration test --- .../api.mapbox.com_27f7da637a8e576c59a3128575847a7abc2d13ba | 1 + .../api.mapbox.com_5c71bf49f4586edeacc48e9b5d1ab218054d423d | 1 + .../api.mapbox.com_798771ac350f81cfa1f5e499d3a6d7a4df71525f | 1 + .../api.mapbox.com_f42b62632d0fa8733ae9eda5e6cb3c9091935380 | 1 + 4 files changed, 4 insertions(+) create mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_27f7da637a8e576c59a3128575847a7abc2d13ba create mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_5c71bf49f4586edeacc48e9b5d1ab218054d423d create mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_798771ac350f81cfa1f5e499d3a6d7a4df71525f create mode 100644 src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_f42b62632d0fa8733ae9eda5e6cb3c9091935380 diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_27f7da637a8e576c59a3128575847a7abc2d13ba b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_27f7da637a8e576c59a3128575847a7abc2d13ba new file mode 100644 index 000000000..867689ad1 --- /dev/null +++ b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_27f7da637a8e576c59a3128575847a7abc2d13ba @@ -0,0 +1 @@ +s:285:"{"type":"FeatureCollection","features":[],"attribution":"NOTICE: © 2026 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_5c71bf49f4586edeacc48e9b5d1ab218054d423d b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_5c71bf49f4586edeacc48e9b5d1ab218054d423d new file mode 100644 index 000000000..86e710744 --- /dev/null +++ b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_5c71bf49f4586edeacc48e9b5d1ab218054d423d @@ -0,0 +1 @@ +s:10311:"{"type":"FeatureCollection","features":[{"type":"Feature","id":"dXJuOm1ieGFkcjo2YjE5MDRmMi1lYjBkLTRjZDQtODViZC1mNmUzZmI1ZTNiZWE","geometry":{"type":"Point","coordinates":[-0.12767,51.50354]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjo2YjE5MDRmMi1lYjBkLTRjZDQtODViZC1mNmUzZmI1ZTNiZWE","feature_type":"address","full_address":"10 Downing Street, City of Westminster, London, SW1A 2AA, United Kingdom","name":"10 Downing Street","name_preferred":"10 Downing Street","coordinates":{"longitude":-0.12767,"latitude":51.50354,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":51.503257,"longitude":-0.12772}]},"place_formatted":"City of Westminster, London, SW1A 2AA, United Kingdom","match_code":{"address_number":"matched","street":"matched","postcode":"unmatched","place":"matched","region":"not_applicable","locality":"unmatched","country":"inferred","confidence":"low"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjo2YjE5MDRmMi1lYjBkLTRjZDQtODViZC1mNmUzZmI1ZTNiZWE","address_number":"10","street_name":"Downing Street","name":"10 Downing Street"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6NmIxOTA0ZjItZWIwZC00Y2Q0LTg1YmQtZjZlM2ZiNWUzYmVh","name":"Downing Street"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpBeFlNVHc","name":"Whitehall","translations":{"en":{"language":"en","name":"Whitehall"}}},"postcode":{"mapbox_id":"postcode.8654501461090586","name":"SW1A 2AA"},"locality":{"mapbox_id":"dXJuOm1ieHBsYzpDM3RLVHc","name":"City of Westminster","wikidata_id":"Q179351","translations":{"en":{"language":"en","name":"City of Westminster"}}},"place":{"mapbox_id":"dXJuOm1ieHBsYzphaWhQ","name":"London","wikidata_id":"Q84","translations":{"en":{"language":"en","name":"London"}}},"district":{"mapbox_id":"dXJuOm1ieHBsYzpDUVpQ","name":"Greater London","wikidata_id":"Q23306","translations":{"en":{"language":"en","name":"Greater London"}}},"region":{"mapbox_id":"dXJuOm1ieHBsYzpKRTg","name":"England","wikidata_id":"Q21","region_code":"ENG","region_code_full":"GB-ENG","translations":{"en":{"language":"en","name":"England"}}},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJazg","name":"United Kingdom","wikidata_id":"Q145","country_code":"GB","country_code_alpha_3":"GBR","translations":{"en":{"language":"en","name":"United Kingdom"}}}}}},{"type":"Feature","id":"dXJuOm1ieGFkcjpjZDYxNzhjMS02NDhhLTQ4OWItYTA2ZS1mYTkzZTExZmU2NDA","geometry":{"type":"Point","coordinates":[0.066,51.51288]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjpjZDYxNzhjMS02NDhhLTQ4OWItYTA2ZS1mYTkzZTExZmU2NDA","feature_type":"address","full_address":"10 Downings, London Borough of Newham, London, E6 6WP, United Kingdom","name":"10 Downings","name_preferred":"10 Downings","coordinates":{"longitude":0.066,"latitude":51.51288,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":51.512731,"longitude":0.065978}]},"place_formatted":"London Borough of Newham, London, E6 6WP, United Kingdom","match_code":{"address_number":"matched","street":"matched","postcode":"unmatched","place":"matched","region":"not_applicable","locality":"unmatched","country":"inferred","confidence":"low"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjpjZDYxNzhjMS02NDhhLTQ4OWItYTA2ZS1mYTkzZTExZmU2NDA","address_number":"10","street_name":"Downings","name":"10 Downings"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6Y2Q2MTc4YzEtNjQ4YS00ODliLWEwNmUtZmE5M2UxMWZlNjQw","name":"Downings"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzoyRXhQ","name":"East Ham","translations":{"en":{"language":"en","name":"East Ham"}}},"postcode":{"mapbox_id":"postcode.5087212628037556","name":"E6 6WP"},"locality":{"mapbox_id":"dXJuOm1ieHBsYzpCN1JxVHc","name":"London Borough of Newham","wikidata_id":"Q208139","translations":{"en":{"language":"en","name":"London Borough of Newham"}}},"place":{"mapbox_id":"dXJuOm1ieHBsYzphaWhQ","name":"London","wikidata_id":"Q84","translations":{"en":{"language":"en","name":"London"}}},"district":{"mapbox_id":"dXJuOm1ieHBsYzpDUVpQ","name":"Greater London","wikidata_id":"Q23306","translations":{"en":{"language":"en","name":"Greater London"}}},"region":{"mapbox_id":"dXJuOm1ieHBsYzpKRTg","name":"England","wikidata_id":"Q21","region_code":"ENG","region_code_full":"GB-ENG","translations":{"en":{"language":"en","name":"England"}}},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJazg","name":"United Kingdom","wikidata_id":"Q145","country_code":"GB","country_code_alpha_3":"GBR","translations":{"en":{"language":"en","name":"United Kingdom"}}}}}},{"type":"Feature","id":"dXJuOm1ieGFkcjo2MWJmMDJlMC04Yzk5LTRjMjItYjAwYS1jOTYxMWNmYmRmNmE","geometry":{"type":"Point","coordinates":[0.12269,52.20363]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjo2MWJmMDJlMC04Yzk5LTRjMjItYjAwYS1jOTYxMWNmYmRmNmE","feature_type":"address","full_address":"10 Downing Street, Cambridge, CB2 3DS, United Kingdom","name":"10 Downing Street","name_preferred":"10 Downing Street","coordinates":{"longitude":0.12269,"latitude":52.20363,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":52.203383,"longitude":0.122846}]},"place_formatted":"Cambridge, CB2 3DS, United Kingdom","match_code":{"address_number":"matched","street":"matched","postcode":"unmatched","place":"unmatched","region":"not_applicable","locality":"not_applicable","country":"inferred","confidence":"low"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjo2MWJmMDJlMC04Yzk5LTRjMjItYjAwYS1jOTYxMWNmYmRmNmE","address_number":"10","street_name":"Downing Street","name":"10 Downing Street"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6NjFiZjAyZTAtOGM5OS00YzIyLWIwMGEtYzk2MTFjZmJkZjZh","name":"Downing Street"},"postcode":{"mapbox_id":"postcode.5786147553339014","name":"CB2 3DS"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpJZ2hQ","name":"Cambridge","wikidata_id":"Q350","translations":{"en":{"language":"en","name":"Cambridge"}}},"district":{"mapbox_id":"dXJuOm1ieHBsYzpBd1pQ","name":"Cambridgeshire","wikidata_id":"Q23112","translations":{"en":{"language":"en","name":"Cambridgeshire"}}},"region":{"mapbox_id":"dXJuOm1ieHBsYzpKRTg","name":"England","wikidata_id":"Q21","region_code":"ENG","region_code_full":"GB-ENG","translations":{"en":{"language":"en","name":"England"}}},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJazg","name":"United Kingdom","wikidata_id":"Q145","country_code":"GB","country_code_alpha_3":"GBR","translations":{"en":{"language":"en","name":"United Kingdom"}}}}}},{"type":"Feature","id":"dXJuOm1ieGFkcjo5MTRjM2RjMy04M2E2LTQzNTgtYjg0YS0xYzk4YWRlNGQ2NGU","geometry":{"type":"Point","coordinates":[-0.79926,51.21331]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjo5MTRjM2RjMy04M2E2LTQzNTgtYjg0YS0xYzk4YWRlNGQ2NGU","feature_type":"address","full_address":"10 Downing Street, Farnham, GU9 7PB, United Kingdom","name":"10 Downing Street","name_preferred":"10 Downing Street","coordinates":{"longitude":-0.79926,"latitude":51.21331,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":51.213331,"longitude":-0.79914}]},"place_formatted":"Farnham, GU9 7PB, United Kingdom","match_code":{"address_number":"matched","street":"matched","postcode":"unmatched","place":"unmatched","region":"not_applicable","locality":"not_applicable","country":"inferred","confidence":"low"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjo5MTRjM2RjMy04M2E2LTQzNTgtYjg0YS0xYzk4YWRlNGQ2NGU","address_number":"10","street_name":"Downing Street","name":"10 Downing Street"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6OTE0YzNkYzMtODNhNi00MzU4LWI4NGEtMWM5OGFkZTRkNjRl","name":"Downing Street"},"postcode":{"mapbox_id":"postcode.5586436697038858","name":"GU9 7PB"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpQc2hQ","name":"Farnham","wikidata_id":"Q643160","translations":{"en":{"language":"en","name":"Farnham"}}},"district":{"mapbox_id":"dXJuOm1ieHBsYzpFcVpQ","name":"Surrey","wikidata_id":"Q23276","translations":{"en":{"language":"en","name":"Surrey"}}},"region":{"mapbox_id":"dXJuOm1ieHBsYzpKRTg","name":"England","wikidata_id":"Q21","region_code":"ENG","region_code_full":"GB-ENG","translations":{"en":{"language":"en","name":"England"}}},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJazg","name":"United Kingdom","wikidata_id":"Q145","country_code":"GB","country_code_alpha_3":"GBR","translations":{"en":{"language":"en","name":"United Kingdom"}}}}}},{"type":"Feature","id":"dXJuOm1ieGFkcjphYjdkZjY0MC03OTA5LTRjZTktOWEyOC01NGQ3MjIwN2IxMWE","geometry":{"type":"Point","coordinates":[-1.20259,53.00619]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjphYjdkZjY0MC03OTA5LTRjZTktOWEyOC01NGQ3MjIwN2IxMWE","feature_type":"address","full_address":"10 Downing Street, Nottingham, NG6 8JU, United Kingdom","name":"10 Downing Street","name_preferred":"10 Downing Street","coordinates":{"longitude":-1.20259,"latitude":53.00619,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":53.0062,"longitude":-1.202407}]},"place_formatted":"Nottingham, NG6 8JU, United Kingdom","match_code":{"address_number":"matched","street":"matched","postcode":"unmatched","place":"unmatched","region":"not_applicable","locality":"not_applicable","country":"inferred","confidence":"low"},"context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjphYjdkZjY0MC03OTA5LTRjZTktOWEyOC01NGQ3MjIwN2IxMWE","address_number":"10","street_name":"Downing Street","name":"10 Downing Street"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6YWI3ZGY2NDAtNzkwOS00Y2U5LTlhMjgtNTRkNzIyMDdiMTFh","name":"Downing Street"},"postcode":{"mapbox_id":"postcode.5551195545198608","name":"NG6 8JU"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpmVWhQ","name":"Nottingham","wikidata_id":"Q41262","translations":{"en":{"language":"en","name":"Nottingham"}}},"district":{"mapbox_id":"dXJuOm1ieHBsYzpCS1pQ","name":"City of Nottingham","wikidata_id":"Q21885994","translations":{"en":{"language":"en","name":"City of Nottingham"}}},"region":{"mapbox_id":"dXJuOm1ieHBsYzpKRTg","name":"England","wikidata_id":"Q21","region_code":"ENG","region_code_full":"GB-ENG","translations":{"en":{"language":"en","name":"England"}}},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJazg","name":"United Kingdom","wikidata_id":"Q145","country_code":"GB","country_code_alpha_3":"GBR","translations":{"en":{"language":"en","name":"United Kingdom"}}}}}}],"attribution":"NOTICE: © 2026 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_798771ac350f81cfa1f5e499d3a6d7a4df71525f b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_798771ac350f81cfa1f5e499d3a6d7a4df71525f new file mode 100644 index 000000000..b2896a0d6 --- /dev/null +++ b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_798771ac350f81cfa1f5e499d3a6d7a4df71525f @@ -0,0 +1 @@ +s:2219:"{"type":"FeatureCollection","features":[{"type":"Feature","id":"dXJuOm1ieGFkcjo1N2NhYTE3Mi03MGEyLTQ5YzYtYjk2ZS1jMDhkYjRkNGE5Zjc","geometry":{"type":"Point","coordinates":[-77.036948,38.900488]},"properties":{"mapbox_id":"dXJuOm1ieGFkcjo1N2NhYTE3Mi03MGEyLTQ5YzYtYjk2ZS1jMDhkYjRkNGE5Zjc","feature_type":"address","full_address":"800 16th Street Northwest, Washington, District of Columbia 20006, United States","name":"800 16th Street Northwest","name_preferred":"800 16th Street Northwest","coordinates":{"longitude":-77.036948,"latitude":38.900488,"accuracy":"rooftop","routable_points":[{"name":"default","latitude":38.900488,"longitude":-77.036768}]},"place_formatted":"Washington, District of Columbia 20006, United States","context":{"address":{"mapbox_id":"dXJuOm1ieGFkcjo1N2NhYTE3Mi03MGEyLTQ5YzYtYjk2ZS1jMDhkYjRkNGE5Zjc","address_number":"800","street_name":"16th Street Northwest","name":"800 16th Street Northwest"},"street":{"mapbox_id":"dXJuOm1ieGFkci1zdHI6NTdjYWExNzItNzBhMi00OWM2LWI5NmUtYzA4ZGI0ZDRhOWY3","name":"16th Street Northwest"},"neighborhood":{"mapbox_id":"dXJuOm1ieHBsYzpDb1BzN0E","name":"Dupont Circle","alternate":{"mapbox_id":"dXJuOm1ieHBsYzpCK0xzN0E","name":"Connecticut Avenue","translations":{"en":{"language":"en","name":"Connecticut Avenue"}}},"translations":{"en":{"language":"en","name":"Dupont Circle"}}},"postcode":{"mapbox_id":"postcode.6693630501763360","name":"20006"},"place":{"mapbox_id":"dXJuOm1ieHBsYzpGSmlvN0E","name":"Washington","wikidata_id":"Q61","translations":{"en":{"language":"en","name":"Washington"}}},"region":{"mapbox_id":"dXJuOm1ieHBsYzpCUVRz","name":"District of Columbia","wikidata_id":"Q3551781","region_code":"DC","region_code_full":"US-DC","translations":{"en":{"language":"en","name":"District of Columbia"}}},"country":{"mapbox_id":"dXJuOm1ieHBsYzpJdXc","name":"United States","wikidata_id":"Q30","country_code":"US","country_code_alpha_3":"USA","translations":{"en":{"language":"en","name":"United States"}}}}}}],"attribution":"NOTICE: © 2026 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file diff --git a/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_f42b62632d0fa8733ae9eda5e6cb3c9091935380 b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_f42b62632d0fa8733ae9eda5e6cb3c9091935380 new file mode 100644 index 000000000..867689ad1 --- /dev/null +++ b/src/Provider/Mapbox/Tests/.cached_responses/api.mapbox.com_f42b62632d0fa8733ae9eda5e6cb3c9091935380 @@ -0,0 +1 @@ +s:285:"{"type":"FeatureCollection","features":[],"attribution":"NOTICE: © 2026 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}"; \ No newline at end of file From 00c8b4d1b2558034dbbdc95dbdd25d79ff765f8c Mon Sep 17 00:00:00 2001 From: David Barratt Date: Fri, 28 Aug 2026 21:34:20 +0000 Subject: [PATCH 11/13] style(mapbox): order imports and annotate MapboxAddress in tests --- src/Provider/Mapbox/Tests/MapboxAddressTest.php | 3 +++ src/Provider/Mapbox/Tests/MapboxTest.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Provider/Mapbox/Tests/MapboxAddressTest.php b/src/Provider/Mapbox/Tests/MapboxAddressTest.php index 02c0a1171..9f3f87153 100644 --- a/src/Provider/Mapbox/Tests/MapboxAddressTest.php +++ b/src/Provider/Mapbox/Tests/MapboxAddressTest.php @@ -20,6 +20,7 @@ class MapboxAddressTest extends TestCase { public function testMatchCode(): void { + /** @var MapboxAddress $address */ $address = (new AddressBuilder('mapbox'))->build(MapboxAddress::class); $this->assertNull($address->getMatchCode()); @@ -33,6 +34,7 @@ public function testMatchCode(): void public function testMatchConfidence(): void { + /** @var MapboxAddress $address */ $address = (new AddressBuilder('mapbox'))->build(MapboxAddress::class); $this->assertNull($address->getMatchConfidence()); @@ -45,6 +47,7 @@ public function testMatchConfidence(): void public function testAccuracy(): void { + /** @var MapboxAddress $address */ $address = (new AddressBuilder('mapbox'))->build(MapboxAddress::class); $this->assertNull($address->getAccuracy()); diff --git a/src/Provider/Mapbox/Tests/MapboxTest.php b/src/Provider/Mapbox/Tests/MapboxTest.php index 44c2055aa..ce351a79c 100644 --- a/src/Provider/Mapbox/Tests/MapboxTest.php +++ b/src/Provider/Mapbox/Tests/MapboxTest.php @@ -12,8 +12,8 @@ namespace Geocoder\Provider\Mapbox\Tests; -use Geocoder\IntegrationTest\BaseTestCase; use Geocoder\Exception\InvalidServerResponse; +use Geocoder\IntegrationTest\BaseTestCase; use Geocoder\Model\AddressCollection; use Geocoder\Model\Bounds; use Geocoder\Provider\Mapbox\Mapbox; From b42e9ca4d39d6ecb00009e4c4e0a98c38b1ad46d Mon Sep 17 00:00:00 2001 From: David Barratt Date: Fri, 28 Aug 2026 22:17:00 +0000 Subject: [PATCH 12/13] docs: remove Mapbox v6 plan and design docs --- docs/plans/2026-08-28-mapbox-v6-design.md | 140 -- docs/plans/2026-08-28-mapbox-v6.md | 1816 --------------------- 2 files changed, 1956 deletions(-) delete mode 100644 docs/plans/2026-08-28-mapbox-v6-design.md delete mode 100644 docs/plans/2026-08-28-mapbox-v6.md diff --git a/docs/plans/2026-08-28-mapbox-v6-design.md b/docs/plans/2026-08-28-mapbox-v6-design.md deleted file mode 100644 index 5ba52cf75..000000000 --- a/docs/plans/2026-08-28-mapbox-v6-design.md +++ /dev/null @@ -1,140 +0,0 @@ -# Mapbox Geocoding v6 — Design - -Issue: https://github.com/geocoder-php/Geocoder/issues/1237 -Date: 2026-08-28 -Status: validated - -## Background - -Mapbox released the [Geocoding API v6](https://docs.mapbox.com/api/search/geocoding/) (GA). The v6 -API is more precise, adds **Structured Input** (typed address fields instead of free text), -**Smart Address Match** (`match_code`), point `accuracy`, `routable_points`, and new feature -types. The v5 API keeps running; Mapbox does not force a migration. - -## Decision - -**Upgrade the existing `Geocoder\Provider\Mapbox\Mapbox` provider in place, as a major -version of `geocoder-php/mapbox-provider` (1.x → 2.0.0).** All breaking changes are listed -in the provider CHANGELOG. Users who need v5 pin to `^1.5`. - -Rationale: the issue asks for *the* Mapbox provider to support v6; the repo's deprecation -procedure (`docs/procedures/provider-deprecation.md`) only applies once an API stops -responding, which has not happened for v5 — but the provider may move forward in a major -release. (A parallel `MapboxV6` class was considered and rejected in favor of a clean -major bump.) - -## Verified v6 API behavior (probed against the live API) - -- Forward: `GET https://api.mapbox.com/search/geocode/v6/forward?q={text}` (or structured fields, no `q`) -- Reverse: `GET https://api.mapbox.com/search/geocode/v6/reverse?longitude={lon}&latitude={lat}` -- Invalid token → **401** (v5 used 403). Both map to `InvalidCredentials` in `AbstractHttpProvider`. -- `types=poi` / `types=poi.landmark` → **422** (POI geocoding removed in v6). -- `types` valid values (per the API's own 422 message): `country, region, postcode, district, - place, locality, neighborhood, street, block, address, secondary_address`. `block` is only - accepted in Japan context (`country=jp`), `secondary_address` in US context; the docs list - the 9 globally-safe values. -- Forward `limit=11` → 200 (silently clamped to the max of 10). -- Reverse `limit>1` **requires exactly one** `types` value, else 422. -- `autocomplete` (replaces v5 `fuzzyMatch`, inverted default): prefix-style matching; a typo'd - query (`wahsington`) returns nothing in v6 — there is no typo tolerance; that is what - `match_code` confidence is for. -- Structured Input: `q` is dropped in favor of `address_line1`, `address_number`, `street`, - `block`, `place`, `region`, `postcode`, `locality`, `neighborhood` (plus `country`); Mapbox - recommends `autocomplete=false` for structured input. -- Response: GeoJSON. `properties` contains `mapbox_id`, `feature_type` (string, formerly - `place_type` array), `name`, `full_address`, `coordinates{longitude,latitude,accuracy, - routable_points}`, `match_code{... ,confidence}` (forward only) and `context` as an - **object keyed by feature type** (v5 was an array of `{id,text,short_code}`). - `context.country` uses `country_code`, `context.region` uses `region_code`/`region_code_full`, - `context.address` uses `street_name`/`address_number`. -- `bbox` is documented on non-address features but is **not currently returned** in any of the - probed responses; the parser maps it defensively when present. - -## Public API surface (2.0.0) - -### Constructor (breaking) - -```php -new Mapbox(ClientInterface $client, string $accessToken, ?string $country = null, bool $permanent = false) -``` - -The 4th argument changes from `string $geocodingMode` to `bool $permanent` (v6 `permanent=true` -storage, applied to forward and reverse requests). - -### Constants - -- `FORWARD_ENDPOINT_URL`, `REVERSE_ENDPOINT_URL` (replace `GEOCODE_ENDPOINT_URL_SSL` / - `REVERSE_ENDPOINT_URL_SSL`). -- Types: `country, region, postcode, district, place, locality, neighborhood, street, address` - in `TYPES` (adds `TYPE_STREET`; removes `TYPE_POI` / `TYPE_POI_LANDMARK`). -- `TYPE_BLOCK` (Japan) and `TYPE_SECONDARY_ADDRESS` (US) exposed as constants but **not** in - `TYPES` (conditionally accepted by the API only). -- `STRUCTURED_INPUT_FIELDS`: the data keys that trigger Structured Input. -- Removed: `GEOCODING_MODE_PLACES`, `GEOCODING_MODE_PLACES_PERMANENT`, `GEOCODING_MODES`. - -### Query data keys - -| Key | v6 param | Note | -|---|---|---| -| `location_type` (string\|array) | `types` | kept from v5; default `address` | -| `autocomplete` (bool) | `autocomplete` | replaces `fuzzy_match` (removed) | -| `country` (string) | `country` | kept; also a structured field | -| `proximity` (`"lon,lat"`\|`"ip"`) | `proximity` | new | -| `worldview` (string) | `worldview` | new | -| `address_line1`, `address_number`, `street`, `block`, `place`, `region`, `postcode`, `locality`, `neighborhood` | same | structured input | -| `bounds` → `bbox`, `locale` → `language`, `limit` → `limit` | | kept | - -**Mode selection:** if any structured field is present, `q` is not sent and `autocomplete` -defaults to `false` (unless the user sets it). Otherwise `q` = query text. (The text must -still be non-empty because `GeocodeQuery::create('')` throws; it is simply not sent in -structured mode.) - -### Parameter order (deterministic URLs) - -- Forward: `q` | structured fields (canonical order) → `bbox` → `types` → `autocomplete` → - `proximity` → `worldview` → `country` → `language` → `limit` → `permanent` (if true) → - `access_token` -- Reverse: `longitude` → `latitude` → `types` → `country` → `language` → `limit` → - `permanent` (if true) → `access_token` - -### Response → model mapping - -| Model | v6 source | -|---|---| -| `id` | `properties.mapbox_id` (fallback: feature `id`) | -| `streetName` | `context.address.street_name` → `context.street.name` → `properties.name` (keeps v5 semantics: no house number) | -| `streetNumber` | `context.address.address_number` | -| `formattedAddress` | `properties.full_address` | -| `neighborhood` | `context.neighborhood.name` | -| `resultType` | `[properties.feature_type]` (array kept for BC) | -| `postalCode` / `locality` / `country` (+code) / admin levels | `context.postcode.name`, `context.locality.name` + `place.name` (→ locality + level 1), `context.region.name` + `region_code` (→ level 2), `context.country.name` + `country_code` — same precedence rules as v5 (later context entry wins for locality) | -| `bounds` | feature `bbox` (when present) | -| **new** `matchCode` / `matchConfidence` | `properties.match_code` / `.confidence` | -| **new** `accuracy` | `properties.coordinates.accuracy` | - -Errors/behavior: IP text → `UnsupportedOperation` (unchanged); 401/403 → `InvalidCredentials`, -429 → `QuotaExceeded`, other ≥300 → `InvalidServerResponse` (base class, unchanged); -`getName()` stays `mapbox`; client-side result limit kept. - -## Testing strategy (TDD) - -1. **Mocked unit tests** — no network: - - basics: name, IP rejection ×3, 429, 401, empty reverse response; - - exact-URL assertions via `getMockedHttpClientCallback` (forward basic / all options / - structured / explicit autocomplete override / reverse / reverse with types+country); - - response-parsing tests with inline JSON fixtures (address feature, place feature, street - feature, feature without context, empty features, invalid JSON). -2. **Integration tests with cached responses** (repo convention): 7 real v6 queries recorded - with a real key (see implementation plan for the exact URLs); `CachedResponseClient` - masks the key as `[apikey]` so CI replays the fixtures with the placeholder key. - v5 fixtures are deleted. -3. `composer test` (full suite), `composer analyse` (PHPStan L6), `composer cs` — per - `CONTRIBUTING.md`. - -## Out of scope (YAGNI) - -- Batch geocoding endpoint (`/v6/batch`) -- `entrances` (public preview) -- `routable_points` on the model -- `format=v5` compatibility mode (we parse native v6) -- Deprecation of the v5 API (it still works; no maintainer decision to deprecate) diff --git a/docs/plans/2026-08-28-mapbox-v6.md b/docs/plans/2026-08-28-mapbox-v6.md deleted file mode 100644 index d0a3724d8..000000000 --- a/docs/plans/2026-08-28-mapbox-v6.md +++ /dev/null @@ -1,1816 +0,0 @@ -# Mapbox Geocoding v6 Implementation Plan - -> **REQUIRED SUB-SKILL:** Use the executing-plans skill to implement this plan task-by-task. - -**Goal:** Upgrade `Geocoder\Provider\Mapbox\Mapbox` in place to the Mapbox Geocoding API v6 (forward, reverse, Structured Input), as a major version of `geocoder-php/mapbox-provider`, with all breaking changes documented in its CHANGELOG. - -**Architecture:** The provider keeps its class name, `Provider`/`AbstractHttpProvider` contract, and the `MapboxAddress` model; request building targets the new `/search/geocode/v6/forward` and `/search/geocode/v6/reverse` endpoints, and response parsing maps the v6 GeoJSON `properties`/`context` object onto the existing model (plus new `matchCode`/`matchConfidence`/`accuracy` getters). Tests follow the repo convention: mocked URL-construction + parsing tests plus integration tests replayed from recorded `.cached_responses` fixtures. - -**Tech Stack:** PHP >= 8.2, PHPUnit 9.6, PSR-18 client, `geocoder-php/provider-integration-tests` (`CachedResponseClient`), PHPStan level 6, PHP-CS-Fixer. - -**Design doc:** `docs/plans/2026-08-28-mapbox-v6-design.md` (read it first). - -**Working branch:** `mapbox-v6` (off `master`). All commands run from the repo root -`/Users/david/Sites/geocoder-php/Geocoder` unless stated otherwise. - -**Test command shorthand:** `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` -(add `--filter ` for single tests). The suite runs with -`USE_CACHED_RESPONSES=true` and the placeholder key from `phpunit.xml.dist`. - -**Live API key:** available in the environment as `MAPBOX_GEOCODING_KEY` (only needed if -fixtures must be re-recorded in Task 0; all tests run without it). - ---- - -### Task 0: Baseline + record v6 fixtures (fixtures are test *inputs*, not implementation) - -**Files:** -- Modify (working tree only, committed in Task 9): `src/Provider/Mapbox/Tests/.cached_responses/` - -**Step 0.1: Verify the current (v5) suite passes** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` -Expected: all tests PASS (cached v5 responses). If vendor is missing, run `composer install` first. - -**Step 0.2: Verify the 7 recorded v6 fixtures are present** - -The fixture files were recorded on 2026-08-28 with the live API (working tree: 7 untracked -`api.mapbox.com_*` files in `src/Provider/Mapbox/Tests/.cached_responses/`, plus 3 v5 files -deleted). Verify exactly these names exist: - -``` -api.mapbox.com_06551ee827f3b9635793737514ec6b9d368075c4 (T7 structured, Park City UT) -api.mapbox.com_338a05e2ceeb9a75e55774746945065683127870 (T5 autocomplete=true, "washi") -api.mapbox.com_5e53474a50aa75267953e6d90b2e3472021efbf2 (T1 "149 9th St, San Francisco, CA 94103") -api.mapbox.com_7b8b47952b295102acd4cb1bc0a9536c26c7bc9b (T3 reverse 2.388911,48.8631507) -api.mapbox.com_9e3e5555aca6117bf14dbd86c37ed23cbece335d (T6 autocomplete=false, "washi") -api.mapbox.com_d73a2016dbd70a152bffadf02f4f04d7e4d6a0b7 (T4 "princ" locale it) -api.mapbox.com_fee388586d54f2150af453c6e033e452d85e46a8 (T2 "1600 Pennsylvania Avenue NW, Washington, DC 20500") -``` - -Names are `api.mapbox.com_` of the exact request URL with the token replaced by the -literal string `[apikey]` (how `CachedResponseClient` computes the key in CI). If any file -is missing, re-record with (requires `$MAPBOX_GEOCODING_KEY` in the environment): - -```bash -php -r ' -$key = getenv("MAPBOX_GEOCODING_KEY"); -$dir = "src/Provider/Mapbox/Tests/.cached_responses"; -$urls = [ - "https://api.mapbox.com/search/geocode/v6/forward?" . http_build_query(["q"=>"149 9th St, San Francisco, CA 94103","types"=>"address","limit"=>5,"access_token"=>$key]), - "https://api.mapbox.com/search/geocode/v6/forward?" . http_build_query(["q"=>"1600 Pennsylvania Avenue NW, Washington, DC 20500","types"=>"address","limit"=>5,"access_token"=>$key]), - "https://api.mapbox.com/search/geocode/v6/reverse?" . http_build_query(["longitude"=>2.388911,"latitude"=>48.8631507,"types"=>"address","limit"=>5,"access_token"=>$key]), - "https://api.mapbox.com/search/geocode/v6/forward?" . http_build_query(["q"=>"princ","bbox"=>"-11.36323261153737,35.82809688193029,34.33947713277206,59.05992036364424","types"=>"place,locality,neighborhood","language"=>"it","limit"=>1,"access_token"=>$key]), - "https://api.mapbox.com/search/geocode/v6/forward?" . http_build_query(["q"=>"washi","bbox"=>"-124.83609163,45.54372254,-116.91742984,49.00243912","types"=>"region,neighborhood,street,place,locality","autocomplete"=>"true","limit"=>5,"access_token"=>$key]), - "https://api.mapbox.com/search/geocode/v6/forward?" . http_build_query(["q"=>"washi","bbox"=>"-124.83609163,45.54372254,-116.91742984,49.00243912","types"=>"region,neighborhood,street,place,locality","autocomplete"=>"false","limit"=>5,"access_token"=>$key]), - "https://api.mapbox.com/search/geocode/v6/forward?" . http_build_query(["address_number"=>"2595","street"=>"Lucky John Dr","place"=>"Park City","region"=>"UT","postcode"=>"84060","types"=>"address","autocomplete"=>"false","limit"=>5,"access_token"=>$key]), -]; -foreach ($urls as $u) { - $cached = str_replace($key, "[apikey]", $u); - $ch = curl_init($u); - curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]); - $body = curl_exec($ch); - if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) !== 200) { fwrite(STDERR, "FAILED $u\n"); exit(1); } - file_put_contents("$dir/api.mapbox.com_" . sha1($cached), serialize((string) $body)); - echo sha1($cached), PHP_EOL; -} -' -``` - -Important: the URL parameter **order** above must stay identical to the provider -implementation in Task 6 (it determines the cache key). - -**Step 0.3: Commit design docs** - -```bash -git add docs/plans/2026-08-28-mapbox-v6-design.md docs/plans/2026-08-28-mapbox-v6.md -git commit -m "docs: add design and implementation plan for Mapbox Geocoding v6" -``` - ---- - -### Task 1: `MapboxAddress::getMatchCode()` / `withMatchCode()` - -**Files:** -- Test: `src/Provider/Mapbox/Tests/MapboxAddressTest.php` (create) -- Modify: `src/Provider/Mapbox/Model/MapboxAddress.php` - -**Step 1.1: Write the failing test** (create the file) - -```php -build(MapboxAddress::class); - - $this->assertNull($address->getMatchCode()); - - $matchCode = ['street' => 'matched', 'confidence' => 'exact']; - $address = $address->withMatchCode($matchCode); - - $this->assertSame($matchCode, $address->getMatchCode()); - $this->assertNull($address->withMatchCode($matchCode)->withMatchCode()->getMatchCode()); - } -} -``` - -**Step 1.2: Run test to verify it fails** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php` -Expected: FAIL with `Error: Call to undefined method Geocoder\Provider\Mapbox\Model\MapboxAddress::getMatchCode()` - -**Step 1.3: Write minimal implementation** - -Add to `MapboxAddress.php` (matching the file's untyped-property + docblock style): - -```php - /** - * @var array|null - */ - private $matchCode; - - /** - * @return array|null - */ - public function getMatchCode(): ?array - { - return $this->matchCode; - } - - /** - * @param array|null $matchCode - */ - public function withMatchCode(?array $matchCode = null): self - { - $new = clone $this; - $new->matchCode = $matchCode; - - return $new; - } -``` - -**Step 1.4: Run test to verify it passes** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php` -Expected: PASS - -**Step 1.5: Commit** - -```bash -git add src/Provider/Mapbox/Model/MapboxAddress.php src/Provider/Mapbox/Tests/MapboxAddressTest.php -git commit -m "feat(mapbox): add matchCode to MapboxAddress" -``` - ---- - -### Task 2: `MapboxAddress::getMatchConfidence()` / `withMatchConfidence()` - -**Files:** -- Test: `src/Provider/Mapbox/Tests/MapboxAddressTest.php` -- Modify: `src/Provider/Mapbox/Model/MapboxAddress.php` - -**Step 2.1: Write the failing test** (append to `MapboxAddressTest`) - -```php - public function testMatchConfidence(): void - { - $address = (new AddressBuilder('mapbox'))->build(MapboxAddress::class); - - $this->assertNull($address->getMatchConfidence()); - - $address = $address->withMatchConfidence('exact'); - - $this->assertSame('exact', $address->getMatchConfidence()); - $this->assertNull($address->withMatchConfidence('exact')->withMatchConfidence()->getMatchConfidence()); - } -``` - -**Step 2.2: Run test to verify it fails** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php --filter testMatchConfidence` -Expected: FAIL with `Error: Call to undefined method ...::getMatchConfidence()` - -**Step 2.3: Write minimal implementation** (in `MapboxAddress.php`) - -```php - /** - * @var string|null - */ - private $matchConfidence; - - public function getMatchConfidence(): ?string - { - return $this->matchConfidence; - } - - public function withMatchConfidence(?string $matchConfidence = null): self - { - $new = clone $this; - $new->matchConfidence = $matchConfidence; - - return $new; - } -``` - -**Step 2.4: Run test to verify it passes** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php` -Expected: PASS (2 tests) - -**Step 2.5: Commit** - -```bash -git add src/Provider/Mapbox/Model/MapboxAddress.php src/Provider/Mapbox/Tests/MapboxAddressTest.php -git commit -m "feat(mapbox): add matchConfidence to MapboxAddress" -``` - ---- - -### Task 3: `MapboxAddress::getAccuracy()` / `withAccuracy()` - -**Files:** -- Test: `src/Provider/Mapbox/Tests/MapboxAddressTest.php` -- Modify: `src/Provider/Mapbox/Model/MapboxAddress.php` - -**Step 3.1: Write the failing test** (append to `MapboxAddressTest`) - -```php - public function testAccuracy(): void - { - $address = (new AddressBuilder('mapbox'))->build(MapboxAddress::class); - - $this->assertNull($address->getAccuracy()); - - $address = $address->withAccuracy('rooftop'); - - $this->assertSame('rooftop', $address->getAccuracy()); - $this->assertNull($address->withAccuracy('rooftop')->withAccuracy()->getAccuracy()); - } -``` - -**Step 3.2: Run test to verify it fails** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php --filter testAccuracy` -Expected: FAIL with `Error: Call to undefined method ...::getAccuracy()` - -**Step 3.3: Write minimal implementation** (in `MapboxAddress.php`) - -```php - /** - * @var string|null - */ - private $accuracy; - - public function getAccuracy(): ?string - { - return $this->accuracy; - } - - public function withAccuracy(?string $accuracy = null): self - { - $new = clone $this; - $new->accuracy = $accuracy; - - return $new; - } -``` - -**Step 3.4: Run test to verify it passes** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php` -Expected: PASS (3 tests) - -**Step 3.5: Commit** - -```bash -git add src/Provider/Mapbox/Model/MapboxAddress.php src/Provider/Mapbox/Tests/MapboxAddressTest.php -git commit -m "feat(mapbox): add accuracy to MapboxAddress" -``` - ---- - -### Task 4: Replace the test file with the version-independent tests (green on old code) - -The old `MapboxTest.php` contains v5-specific integration tests (v5 fixtures, `TYPE_POI`, -`fuzzy_match`). They are removed here; their v6 replacements arrive in Task 5. - -**Files:** -- Modify (full rewrite): `src/Provider/Mapbox/Tests/MapboxTest.php` - -**Step 4.1: Replace `MapboxTest.php` with** - -```php -getMockedHttpClient(), 'access_token'); - $this->assertEquals('mapbox', $provider->getName()); - } - - public function testGeocodeWithLocalhostIPv4(): void - { - $this->expectException(\Geocoder\Exception\UnsupportedOperation::class); - $this->expectExceptionMessage('The Mapbox provider does not support IP addresses, only street addresses.'); - - $provider = new Mapbox($this->getMockedHttpClient(), 'access_token'); - $provider->geocodeQuery(GeocodeQuery::create('127.0.0.1')); - } - - public function testGeocodeWithLocalhostIPv6(): void - { - $this->expectException(\Geocoder\Exception\UnsupportedOperation::class); - $this->expectExceptionMessage('The Mapbox provider does not support IP addresses, only street addresses.'); - - $provider = new Mapbox($this->getMockedHttpClient(), 'access_token'); - $provider->geocodeQuery(GeocodeQuery::create('::1')); - } - - public function testGeocodeWithRealIp(): void - { - $this->expectException(\Geocoder\Exception\UnsupportedOperation::class); - $this->expectExceptionMessage('The Mapbox provider does not support IP addresses, only street addresses.'); - - $provider = new Mapbox($this->getHttpClient(), 'access_token'); - $provider->geocodeQuery(GeocodeQuery::create('74.200.247.59')); - } - - public function testGeocodeWithQuotaExceeded(): void - { - $this->expectException(\Geocoder\Exception\QuotaExceeded::class); - - $provider = new Mapbox($this->getMockedHttpClient('', 429), 'access_token'); - $provider->geocodeQuery(GeocodeQuery::create('10 avenue Gambetta, Paris, France')); - } - - public function testGeocodeWithInvalidApiKey(): void - { - // The v6 API answers 401 for an invalid token (v5 answered 403). - // Both status codes map to InvalidCredentials in AbstractHttpProvider. - $this->expectException(\Geocoder\Exception\InvalidCredentials::class); - - $provider = new Mapbox($this->getMockedHttpClient('', 401), 'api_key'); - $provider->geocodeQuery(GeocodeQuery::create('10 avenue Gambetta, Paris, France')); - } - - public function testReverseWithInvalidResponse(): void - { - $this->expectException(InvalidServerResponse::class); - - $provider = new Mapbox($this->getMockedHttpClient(), 'access_token'); - $provider->reverseQuery(ReverseQuery::fromCoordinates(1, 2)); - } -} -``` - -**Step 4.2: Run tests to verify they pass (old v5 implementation still in place)** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` -Expected: PASS (7 tests) — these behaviors are version-independent, so they pass on the old -code. (This commit deliberately leaves the suite without Mapbox integration tests; Task 5 -restores them as v6 tests.) - -**Step 4.3: Commit** - -```bash -git add src/Provider/Mapbox/Tests/MapboxTest.php -git commit -m "test(mapbox): prepare test suite for the v6 API" -``` - ---- - -### Task 5: Add the failing v6 tests (RED) - -**Files:** -- Modify: `src/Provider/Mapbox/Tests/MapboxTest.php` (imports + new methods) -- Modify: `src/Provider/Mapbox/Mapbox.php` (ONLY the new constants — Step 5.3, required so - the test file can load at all; the rest of the implementation arrives in Tasks 6–7) - -**Step 5.1: Extend the `use` block** (top of `MapboxTest.php`, final state) - -```php -use Geocoder\IntegrationTest\BaseTestCase; -use Geocoder\Exception\InvalidServerResponse; -use Geocoder\Model\AddressCollection; -use Geocoder\Model\Bounds; -use Geocoder\Provider\Mapbox\Mapbox; -use Geocoder\Provider\Mapbox\Model\MapboxAddress; -use Geocoder\Query\GeocodeQuery; -use Geocoder\Query\ReverseQuery; -use Psr\Http\Message\RequestInterface; -``` - -**Step 5.2: Add the URL-construction tests** (before the final `}` of the class) - -```php - private function assertRequestUrl(string $expectedUrl, callable $request): void - { - $uri = ''; - $client = $this->getMockedHttpClientCallback(function (RequestInterface $request) use (&$uri) { - $uri = (string) $request->getUri(); - }); - - try { - $request($client); - } catch (InvalidServerResponse $e) { - // the mocked client answers an empty body; we only assert the request - } - - $this->assertSame($expectedUrl, $uri); - } - - public function testForwardGeocodeUrl(): void - { - $this->assertRequestUrl( - 'https://api.mapbox.com/search/geocode/v6/forward' - .'?q=10+Downing+St%2C+London' - .'&types=address' - .'&limit=5' - .'&access_token=access_token', - static function ($client) { - (new Mapbox($client, 'access_token'))->geocodeQuery(GeocodeQuery::create('10 Downing St, London')); - } - ); - } - - public function testForwardGeocodeUrlWithAllOptions(): void - { - $this->assertRequestUrl( - 'https://api.mapbox.com/search/geocode/v6/forward' - .'?q=wahsington' - .'&bbox=-124.83609163%2C45.54372254%2C-116.91742984%2C49.00243912' - .'&types=address%2Cstreet' - .'&autocomplete=true' - .'&proximity=-120.09%2C47.60' - .'&worldview=us' - .'&country=US' - .'&language=en' - .'&limit=10' - .'&permanent=true' - .'&access_token=access_token', - static function ($client) { - $provider = new Mapbox($client, 'access_token', 'US', true); - $query = GeocodeQuery::create('wahsington') - ->withLocale('en') - ->withBounds(new Bounds(45.54372254, -124.83609163, 49.00243912, -116.91742984)) - ->withLimit(10) - ->withData('location_type', [Mapbox::TYPE_ADDRESS, Mapbox::TYPE_STREET]) - ->withData('autocomplete', true) - ->withData('proximity', '-120.09,47.60') - ->withData('worldview', 'us'); - $provider->geocodeQuery($query); - } - ); - } - - public function testForwardGeocodeUrlWithStructuredInput(): void - { - // Structured Input: the query text is NOT sent; fields use their v6 names. - // autocomplete defaults to false for structured input (Mapbox guidance). - $this->assertRequestUrl( - 'https://api.mapbox.com/search/geocode/v6/forward' - .'?address_number=2595' - .'&street=Lucky+John+Dr' - .'&place=Park+City' - .'®ion=UT' - .'&postcode=84060' - .'&types=address' - .'&autocomplete=false' - .'&country=US' - .'&limit=5' - .'&access_token=access_token', - static function ($client) { - $provider = new Mapbox($client, 'access_token'); - $query = GeocodeQuery::create('2595 Lucky John Dr, Park City, UT 84060') - ->withData('address_number', '2595') - ->withData('street', 'Lucky John Dr') - ->withData('place', 'Park City') - ->withData('region', 'UT') - ->withData('postcode', '84060') - ->withData('country', 'US'); - $provider->geocodeQuery($query); - } - ); - } - - public function testForwardGeocodeUrlWithStructuredInputAndExplicitAutocomplete(): void - { - // an explicit autocomplete value overrides the structured-input default - $this->assertRequestUrl( - 'https://api.mapbox.com/search/geocode/v6/forward' - .'?street=9th+Street' - .'&types=address' - .'&autocomplete=true' - .'&limit=5' - .'&access_token=access_token', - static function ($client) { - $provider = new Mapbox($client, 'access_token'); - $query = GeocodeQuery::create('9th Street') - ->withData('street', '9th Street') - ->withData('autocomplete', true); - $provider->geocodeQuery($query); - } - ); - } - - public function testReverseGeocodeUrl(): void - { - $this->assertRequestUrl( - 'https://api.mapbox.com/search/geocode/v6/reverse' - .'?longitude=2.388911' - .'&latitude=48.8631507' - .'&types=address' - .'&limit=5' - .'&access_token=access_token', - static function ($client) { - (new Mapbox($client, 'access_token'))->reverseQuery(ReverseQuery::fromCoordinates(48.8631507, 2.388911)); - } - ); - } - - public function testReverseGeocodeUrlWithLocationTypeAndCountry(): void - { - $this->assertRequestUrl( - 'https://api.mapbox.com/search/geocode/v6/reverse' - .'?longitude=2.388911' - .'&latitude=48.8631507' - .'&types=address%2Cpostcode' - .'&country=FR' - .'&limit=2' - .'&access_token=access_token', - static function ($client) { - $provider = new Mapbox($client, 'access_token'); - $query = ReverseQuery::fromCoordinates(48.8631507, 2.388911) - ->withData('location_type', [Mapbox::TYPE_ADDRESS, Mapbox::TYPE_POSTCODE]) - ->withData('country', 'FR') - ->withLimit(2); - $provider->reverseQuery($query); - } - ); - } -``` - -(The remaining test methods — response parsing + integration — are in the section below; -append them before the final `}` as well.) - -**Step 5.3: Add the new constants so the test file can load** - -Minimal production code required for the suite to run (tests reference -`Mapbox::TYPE_STREET` / `Mapbox::TYPE_POSTCODE`). Append to the constants block of -`src/Provider/Mapbox/Mapbox.php`: - -```php - /** - * @var string - */ - public const TYPE_STREET = 'street'; - - /** - * Japanese addresses only. The API accepts this type only for Japan-context requests - * (e.g. with country=jp and language=ja), so it is intentionally not part of TYPES. - * - * @var string - */ - public const TYPE_BLOCK = 'block'; - - /** - * Sub-unit, suite, or lot within a parent address (US only). The API accepts this type - * only for US-context requests, so it is intentionally not part of TYPES. - * - * @var string - */ - public const TYPE_SECONDARY_ADDRESS = 'secondary_address'; - - /** - * Query data keys that trigger the v6 Structured Input mode (the query text is not sent). - * - * @var string[] - */ - public const STRUCTURED_INPUT_FIELDS = [ - 'address_line1', - 'address_number', - 'street', - 'block', - 'place', - 'region', - 'postcode', - 'locality', - 'neighborhood', - ]; -``` - -**Step 5.4: Run the tests to verify they FAIL for the right reasons (RED)** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` -Expected: -- the 7 version-independent tests PASS; -- the 6 URL-construction tests FAIL (old v5 URLs / 4th constructor arg still a string); -- the parsing tests FAIL (old v5 parser cannot handle the v6 `context` object); -- the 7 integration tests FAIL (provider builds v5 URLs → cache miss → live call with the - placeholder key → 401 → `InvalidCredentials`). - -**Step 5.5: Commit the failing tests** - -```bash -git add src/Provider/Mapbox/Tests/MapboxTest.php src/Provider/Mapbox/Mapbox.php -git commit -m "test(mapbox): add failing tests for the v6 API" -``` -**Step 5.2 (continued): append the response-parsing and integration tests** (before the -final `}`, after the URL-construction tests) - -## Response parsing (mocked v6 JSON) - -```php - public function testParseForwardAddressFeature(): void - { - $json = <<<'JSON' - { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "id": "dXJuOm1ieGFkcjo3Njg3YjZmNy01YmZkLTQzMjItOGMzOS02OGE1NDhmZWYwM2U", - "geometry": { "type": "Point", "coordinates": [-122.413709, 37.7757] }, - "properties": { - "mapbox_id": "dXJuOm1ieGFkcjo3Njg3YjZmNy01YmZkLTQzMjItOGMzOS02OGE1NDhmZWYwM2U", - "feature_type": "address", - "name": "149 9th Street", - "full_address": "149 9th Street, San Francisco, California 94103, United States", - "coordinates": { "longitude": -122.413709, "latitude": 37.7757, "accuracy": "rooftop" }, - "match_code": { "address_number": "matched", "street": "matched", "confidence": "exact" }, - "context": { - "address": { "address_number": "149", "street_name": "9th Street", "name": "149 9th Street" }, - "street": { "name": "9th Street" }, - "neighborhood": { "name": "South of Market" }, - "postcode": { "name": "94103" }, - "place": { "name": "San Francisco" }, - "region": { "name": "California", "region_code": "CA", "region_code_full": "US-CA" }, - "country": { "name": "United States", "country_code": "US", "country_code_alpha_3": "USA" } - } - } - } - ] - } - JSON; - - $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); - $results = $provider->geocodeQuery(GeocodeQuery::create('149 9th St, San Francisco, CA 94103')); - - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(1, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertInstanceOf(MapboxAddress::class, $result); - $this->assertEqualsWithDelta(37.7757, $result->getCoordinates()->getLatitude(), 0.00001); - $this->assertEqualsWithDelta(-122.413709, $result->getCoordinates()->getLongitude(), 0.00001); - $this->assertEquals('dXJuOm1ieGFkcjo3Njg3YjZmNy01YmZkLTQzMjItOGMzOS02OGE1NDhmZWYwM2U', $result->getId()); - $this->assertEquals('9th Street', $result->getStreetName()); - $this->assertEquals('149', $result->getStreetNumber()); - $this->assertEquals('94103', $result->getPostalCode()); - $this->assertEquals('San Francisco', $result->getLocality()); - $this->assertCount(2, $result->getAdminLevels()); - $this->assertEquals('San Francisco', $result->getAdminLevels()->get(1)->getName()); - $this->assertEquals('California', $result->getAdminLevels()->get(2)->getName()); - $this->assertEquals('CA', $result->getAdminLevels()->get(2)->getCode()); - $this->assertEquals('United States', $result->getCountry()->getName()); - $this->assertEquals('US', $result->getCountry()->getCode()); - $this->assertEquals('149 9th Street, San Francisco, California 94103, United States', $result->getFormattedAddress()); - $this->assertEquals('South of Market', $result->getNeighborhood()); - $this->assertEquals(['address'], $result->getResultType()); - $this->assertEquals('exact', $result->getMatchConfidence()); - $this->assertEquals('matched', $result->getMatchCode()['street']); - $this->assertEquals('rooftop', $result->getAccuracy()); - $this->assertNull($result->getTimezone()); - } - - public function testParseForwardPlaceFeature(): void - { - // place feature: no context.address; name is the place name; region code may be 3 letters - $json = <<<'JSON' - { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "id": "dXJuOm1ieHBsYzpoMmhQ", - "geometry": { "type": "Point", "coordinates": [-0.83069, 51.724422] }, - "properties": { - "mapbox_id": "dXJuOm1ieHBsYzpoMmhQ", - "feature_type": "place", - "name": "Princes Risborough", - "full_address": "Princes Risborough, Buckinghamshire, Inghilterra, Regno Unito", - "coordinates": { "longitude": -0.83069, "latitude": 51.724422 }, - "context": { - "place": { "name": "Princes Risborough" }, - "region": { "name": "Inghilterra", "region_code": "ENG" }, - "country": { "name": "Regno Unito", "country_code": "GB" } - } - } - } - ] - } - JSON; - - $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); - $results = $provider->geocodeQuery(GeocodeQuery::create('princ')); - - $this->assertCount(1, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertEquals('dXJuOm1ieHBsYzpoMmhQ', $result->getId()); - $this->assertEquals('Princes Risborough', $result->getStreetName()); - $this->assertNull($result->getStreetNumber()); - $this->assertNull($result->getPostalCode()); - $this->assertEquals('Princes Risborough', $result->getLocality()); - $this->assertCount(2, $result->getAdminLevels()); - $this->assertEquals('Princes Risborough', $result->getAdminLevels()->get(1)->getName()); - $this->assertEquals('Inghilterra', $result->getAdminLevels()->get(2)->getName()); - $this->assertEquals('ENG', $result->getAdminLevels()->get(2)->getCode()); - $this->assertEquals('Regno Unito', $result->getCountry()->getName()); - $this->assertEquals('GB', $result->getCountry()->getCode()); - $this->assertEquals('Princes Risborough, Buckinghamshire, Inghilterra, Regno Unito', $result->getFormattedAddress()); - $this->assertNull($result->getNeighborhood()); - $this->assertEquals(['place'], $result->getResultType()); - $this->assertNull($result->getMatchConfidence()); - $this->assertNull($result->getMatchCode()); - $this->assertNull($result->getAccuracy()); - } - - public function testParseForwardStreetFeature(): void - { - // v6 street feature: street name comes from context.street - $json = <<<'JSON' - { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "id": "address.7607992201284362", - "geometry": { "type": "Point", "coordinates": [-117.039767, 46.930253] }, - "properties": { - "mapbox_id": "address.7607992201284362", - "feature_type": "street", - "name": "Washington Highway 6", - "full_address": "Washington Highway 6, Palouse, Washington 99161, United States", - "coordinates": { "longitude": -117.039767, "latitude": 46.930253 }, - "context": { - "street": { "name": "Washington Highway 6" }, - "postcode": { "name": "99161" }, - "place": { "name": "Palouse" }, - "region": { "name": "Washington", "region_code": "WA" }, - "country": { "name": "United States", "country_code": "US" } - } - } - } - ] - } - JSON; - - $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); - $results = $provider->geocodeQuery(GeocodeQuery::create('Washington Highway 6')); - - $this->assertCount(1, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertEquals('address.7607992201284362', $result->getId()); - $this->assertEquals('Washington Highway 6', $result->getStreetName()); - $this->assertNull($result->getStreetNumber()); - $this->assertEquals('99161', $result->getPostalCode()); - $this->assertEquals('Palouse', $result->getLocality()); - $this->assertEquals('Washington', $result->getAdminLevels()->get(2)->getName()); - $this->assertEquals('WA', $result->getAdminLevels()->get(2)->getCode()); - $this->assertEquals(['street'], $result->getResultType()); - } - - public function testParseFeatureWithoutContext(): void - { - // features without a context object must still parse (name + coordinates) - $json = <<<'JSON' - { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "id": "abc123", - "geometry": { "type": "Point", "coordinates": [2.35, 48.85] }, - "properties": { - "mapbox_id": "abc123", - "feature_type": "place", - "name": "Some Place", - "coordinates": { "longitude": 2.35, "latitude": 48.85 } - } - } - ] - } - JSON; - - $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); - $results = $provider->geocodeQuery(GeocodeQuery::create('Some Place')); - - $this->assertCount(1, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertEquals('Some Place', $result->getStreetName()); - $this->assertEquals('abc123', $result->getId()); - $this->assertEqualsWithDelta(48.85, $result->getCoordinates()->getLatitude(), 0.00001); - $this->assertEqualsWithDelta(2.35, $result->getCoordinates()->getLongitude(), 0.00001); - } - - public function testParseEmptyFeatures(): void - { - $json = '{"type":"FeatureCollection","features":[]}'; - - $provider = new Mapbox($this->getMockedHttpClient($json), 'access_token'); - $results = $provider->geocodeQuery(GeocodeQuery::create('jsajhgsdkfjhsfkjhaldkadjaslgldasd')); - - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertEmpty($results); - } - - public function testParseInvalidJson(): void - { - $this->expectException(InvalidServerResponse::class); - - $provider = new Mapbox($this->getMockedHttpClient('{invalid json'), 'access_token'); - $provider->geocodeQuery(GeocodeQuery::create('10 Downing St, London')); - } -``` - -## Integration tests (replayed from `.cached_responses`) - -All seven use the standard guard + client construction: - -```php - if (!isset($_SERVER['MAPBOX_GEOCODING_KEY'])) { - $this->markTestSkipped('You need to configure the MAPBOX_GEOCODING_KEY value in phpunit.xml'); - } - - $provider = new Mapbox($this->getHttpClient($_SERVER['MAPBOX_GEOCODING_KEY']), $_SERVER['MAPBOX_GEOCODING_KEY']); -``` - -```php - public function testGeocodeWithRealAddress(): void - { - // guard + $provider as above - $results = $provider->geocodeQuery(GeocodeQuery::create('149 9th St, San Francisco, CA 94103')); - - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(5, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertInstanceOf(MapboxAddress::class, $result); - $this->assertEqualsWithDelta(37.7757, $result->getCoordinates()->getLatitude(), 0.001); - $this->assertEqualsWithDelta(-122.413709, $result->getCoordinates()->getLongitude(), 0.001); - $this->assertEquals('9th Street', $result->getStreetName()); - $this->assertEquals('149', $result->getStreetNumber()); - $this->assertEquals('94103', $result->getPostalCode()); - $this->assertEquals('San Francisco', $result->getLocality()); - $this->assertCount(2, $result->getAdminLevels()); - $this->assertEquals('California', $result->getAdminLevels()->get(2)->getName()); - $this->assertEquals('CA', $result->getAdminLevels()->get(2)->getCode()); - $this->assertEquals('United States', $result->getCountry()->getName()); - $this->assertEquals('US', $result->getCountry()->getCode()); - $this->assertEquals('149 9th Street, San Francisco, California 94103, United States', $result->getFormattedAddress()); - $this->assertEquals('South of Market', $result->getNeighborhood()); - $this->assertEquals(['address'], $result->getResultType()); - $this->assertEquals('exact', $result->getMatchConfidence()); - $this->assertEquals('rooftop', $result->getAccuracy()); - - // not provided - $this->assertNull($result->getTimezone()); - } - - public function testGeocodeWithRealAddressInDc(): void - { - // guard + $provider as above - $results = $provider->geocodeQuery(GeocodeQuery::create('1600 Pennsylvania Avenue NW, Washington, DC 20500')); - - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(2, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertInstanceOf(MapboxAddress::class, $result); - $this->assertEqualsWithDelta(38.897684, $result->getCoordinates()->getLatitude(), 0.001); - $this->assertEqualsWithDelta(-77.036574, $result->getCoordinates()->getLongitude(), 0.001); - $this->assertEquals('Pennsylvania Avenue Northwest', $result->getStreetName()); - $this->assertEquals('1600', $result->getStreetNumber()); - $this->assertEquals('20500', $result->getPostalCode()); - $this->assertEquals('Washington', $result->getLocality()); - $this->assertCount(2, $result->getAdminLevels()); - $this->assertEquals('District of Columbia', $result->getAdminLevels()->get(2)->getName()); - $this->assertEquals('DC', $result->getAdminLevels()->get(2)->getCode()); - $this->assertEquals('United States', $result->getCountry()->getName()); - $this->assertEquals('US', $result->getCountry()->getCode()); - $this->assertEquals('1600 Pennsylvania Avenue Northwest, Washington, District of Columbia 20500, United States', $result->getFormattedAddress()); - $this->assertEquals('exact', $result->getMatchConfidence()); - $this->assertEquals('rooftop', $result->getAccuracy()); - } - - public function testReverseWithRealCoordinates(): void - { - // guard + $provider as above - $results = $provider->reverseQuery(ReverseQuery::fromCoordinates(48.8631507, 2.388911)); - - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(4, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertInstanceOf(MapboxAddress::class, $result); - $this->assertEquals('12', $result->getStreetNumber()); - $this->assertEquals('Avenue Gambetta', $result->getStreetName()); - $this->assertEquals('75020', $result->getPostalCode()); - $this->assertEquals('Paris', $result->getLocality()); - $this->assertCount(2, $result->getAdminLevels()); - $this->assertEquals('Paris', $result->getAdminLevels()->get(1)->getName()); - $this->assertEquals('Île-de-France', $result->getAdminLevels()->get(2)->getName()); - $this->assertEquals('IDF', $result->getAdminLevels()->get(2)->getCode()); - $this->assertEquals('France', $result->getCountry()->getName()); - $this->assertEquals('FR', $result->getCountry()->getCode()); - $this->assertEquals('12 Avenue Gambetta, 75020 Paris, France', $result->getFormattedAddress()); - $this->assertEquals('rooftop', $result->getAccuracy()); - - // not provided by reverse geocoding - $this->assertNull($result->getMatchConfidence()); - $this->assertNull($result->getNeighborhood()); - } - - public function testGeocodePlaceWithLocale(): void - { - // guard + $provider as above - $query = GeocodeQuery::create('princ'); - $query = $query->withLocale('it'); - $query = $query->withBounds(new Bounds( - 35.82809688193029, - -11.36323261153737, - 59.05992036364424, - 34.33947713277206 - )); - $query = $query->withLimit(1); - $query = $query->withData('location_type', [ - Mapbox::TYPE_PLACE, - Mapbox::TYPE_LOCALITY, - Mapbox::TYPE_NEIGHBORHOOD, - ]); - - $results = $provider->geocodeQuery($query); - - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(1, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertInstanceOf(MapboxAddress::class, $result); - $this->assertEqualsWithDelta(51.724422, $result->getCoordinates()->getLatitude(), 0.001); - $this->assertEqualsWithDelta(-0.83069, $result->getCoordinates()->getLongitude(), 0.001); - $this->assertEquals('Princes Risborough', $result->getStreetName()); - $this->assertEquals(['place'], $result->getResultType()); - $this->assertEquals('Princes Risborough', $result->getLocality()); - $this->assertCount(2, $result->getAdminLevels()); - $this->assertEquals('Princes Risborough', $result->getAdminLevels()->get(1)->getName()); - $this->assertEquals('Inghilterra', $result->getAdminLevels()->get(2)->getName()); - $this->assertEquals('ENG', $result->getAdminLevels()->get(2)->getCode()); - $this->assertEquals('Regno Unito', $result->getCountry()->getName()); - $this->assertEquals('GB', $result->getCountry()->getCode()); - - // not provided - $this->assertNull($result->getPostalCode()); - $this->assertNull($result->getStreetNumber()); - $this->assertNull($result->getMatchConfidence()); - } - - public function testGeocodeWithAutocompleteEnabled(): void - { - // guard + $provider as above - $query = GeocodeQuery::create('washi'); - $query = $query->withData('autocomplete', true); - $query = $query->withBounds(new Bounds( - 45.54372254, - -124.83609163, - 49.00243912, - -116.91742984 - )); - $query = $query->withData('location_type', [ - Mapbox::TYPE_REGION, - Mapbox::TYPE_NEIGHBORHOOD, - Mapbox::TYPE_STREET, - Mapbox::TYPE_PLACE, - Mapbox::TYPE_LOCALITY, - ]); - - $results = $provider->geocodeQuery($query); - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(5, $results); - - /** @var MapboxAddress $first */ - $first = $results->first(); - $this->assertEquals(['region'], $first->getResultType()); - $this->assertEquals('Washington', $first->getStreetName()); - $this->assertEquals('Washington', $first->getAdminLevels()->get(2)->getName()); - $this->assertEquals('WA', $first->getAdminLevels()->get(2)->getCode()); - } - - public function testGeocodeWithAutocompleteDisabled(): void - { - // guard + $provider as above - $query = GeocodeQuery::create('washi'); - $query = $query->withData('autocomplete', false); - $query = $query->withBounds(new Bounds( - 45.54372254, - -124.83609163, - 49.00243912, - -116.91742984 - )); - $query = $query->withData('location_type', [ - Mapbox::TYPE_REGION, - Mapbox::TYPE_NEIGHBORHOOD, - Mapbox::TYPE_STREET, - Mapbox::TYPE_PLACE, - Mapbox::TYPE_LOCALITY, - ]); - - $results = $provider->geocodeQuery($query); - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(1, $results); - - /** @var MapboxAddress $first */ - $first = $results->first(); - $this->assertEquals(['street'], $first->getResultType()); - $this->assertEquals('Yashi Road', $first->getStreetName()); - } - - public function testGeocodeWithStructuredInput(): void - { - // guard + $provider as above - $query = GeocodeQuery::create('2595 Lucky John Dr, Park City, UT 84060'); - $query = $query->withData('address_number', '2595'); - $query = $query->withData('street', 'Lucky John Dr'); - $query = $query->withData('place', 'Park City'); - $query = $query->withData('region', 'UT'); - $query = $query->withData('postcode', '84060'); - - $results = $provider->geocodeQuery($query); - - $this->assertInstanceOf(AddressCollection::class, $results); - $this->assertCount(1, $results); - - /** @var MapboxAddress $result */ - $result = $results->first(); - $this->assertInstanceOf(MapboxAddress::class, $result); - $this->assertEqualsWithDelta(40.6716, $result->getCoordinates()->getLatitude(), 0.001); - $this->assertEqualsWithDelta(-111.507008, $result->getCoordinates()->getLongitude(), 0.001); - $this->assertEquals('Lucky John Drive', $result->getStreetName()); - $this->assertEquals('2595', $result->getStreetNumber()); - $this->assertEquals('84060', $result->getPostalCode()); - $this->assertEquals('Park City', $result->getLocality()); - $this->assertCount(2, $result->getAdminLevels()); - $this->assertEquals('Park City', $result->getAdminLevels()->get(1)->getName()); - $this->assertEquals('Utah', $result->getAdminLevels()->get(2)->getName()); - $this->assertEquals('UT', $result->getAdminLevels()->get(2)->getCode()); - $this->assertEquals('United States', $result->getCountry()->getName()); - $this->assertEquals('US', $result->getCountry()->getCode()); - $this->assertEquals('2595 Lucky John Drive, Park City, Utah 84060, United States', $result->getFormattedAddress()); - $this->assertEquals('Park Meadows', $result->getNeighborhood()); - $this->assertEquals('exact', $result->getMatchConfidence()); - $this->assertEquals('rooftop', $result->getAccuracy()); - } -``` - -**Step 5.3: Add the new constants so the test file can load** - -This is the minimal production code required for the suite to run at all (tests reference -`Mapbox::TYPE_STREET` etc.). Insert into `src/Provider/Mapbox/Mapbox.php` after -`TYPE_ADDRESS`: - -```php - /** - * @var string - */ - public const TYPE_STREET = 'street'; - - /** - * Japanese addresses only. The API accepts this type only for Japan-context requests - * (e.g. country=jp + language=ja), so it is intentionally not part of TYPES. - * - * @var string - */ - public const TYPE_BLOCK = 'block'; - - /** - * Sub-unit, suite or lot within a parent address (US only). The API accepts this type - * only for US-context requests, so it is intentionally not part of TYPES. - * - * @var string - */ - public const TYPE_SECONDARY_ADDRESS = 'secondary_address'; -``` - -**Step 5.4: Run the suite to verify the NEW tests fail (RED)** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` -Expected: -- the 7 Task-4 tests PASS (still); -- the 6 URL-construction tests FAIL with `Failed asserting that 'https://api.mapbox.com/geocoding/v5/...' === 'https://api.mapbox.com/search/geocode/v6/...'` (provider still builds v5 URLs); -- the 6 parsing tests FAIL (old parser chokes on the v6 `properties`/`context` shape — warnings/assertion failures, not the expected model values); -- the 7 integration tests FAIL with `Geocoder\Exception\InvalidCredentials` (provider builds v5 URLs → cache miss → live call with the placeholder key → 401). - -If a test fails for an unexpected reason (typo in a fixture, wrong assertion), fix the TEST, -re-run, and re-verify the RED state. Do not proceed while the RED state is not as expected. - -**Step 5.5: Commit** - -```bash -git add src/Provider/Mapbox/Tests/MapboxTest.php src/Provider/Mapbox/Mapbox.php -git commit -m "test(mapbox): add failing tests for the v6 API" -``` - ---- - -### Task 6: Implement v6 request building (GREEN for URL tests) - -**Files:** -- Modify: `src/Provider/Mapbox/Mapbox.php` - -**Step 6.1: Replace the endpoint constants** - -```php - /** - * @var string - */ - public const FORWARD_ENDPOINT_URL = 'https://api.mapbox.com/search/geocode/v6/forward'; - - /** - * @var string - */ - public const REVERSE_ENDPOINT_URL = 'https://api.mapbox.com/search/geocode/v6/reverse'; -``` - -(Delete `GEOCODE_ENDPOINT_URL_SSL` and `REVERSE_ENDPOINT_URL_SSL`.) - -**Step 6.2: Replace the mode constants and update TYPES** - -Delete `GEOCODING_MODE_PLACES`, `GEOCODING_MODE_PLACES_PERMANENT`, `GEOCODING_MODES`, -`TYPE_POI`, `TYPE_POI_LANDMARK`. Final types block: - -```php - /** - * @var string - */ - public const TYPE_COUNTRY = 'country'; - // ... (existing TYPE_REGION, TYPE_POSTCODE, TYPE_DISTRICT, TYPE_PLACE, - // TYPE_LOCALITY, TYPE_NEIGHBORHOOD, TYPE_ADDRESS unchanged) - - /** - * @var string[] - */ - public const TYPES = [ - self::TYPE_COUNTRY, - self::TYPE_REGION, - self::TYPE_POSTCODE, - self::TYPE_DISTRICT, - self::TYPE_PLACE, - self::TYPE_LOCALITY, - self::TYPE_NEIGHBORHOOD, - self::TYPE_STREET, - self::TYPE_ADDRESS, - ]; -``` - -Plus the `TYPE_STREET` / `TYPE_BLOCK` / `TYPE_SECONDARY_ADDRESS` constants from Step 5.3 and: - -```php - /** - * Query data keys that trigger the v6 Structured Input mode (the query text is not sent). - * - * @var string[] - */ - public const STRUCTURED_INPUT_FIELDS = [ - 'address_line1', - 'address_number', - 'street', - 'block', - 'place', - 'region', - 'postcode', - 'locality', - 'neighborhood', - ]; -``` - -**Step 6.3: Replace the constructor and the two query methods** - -```php - /** - * @var bool - */ - private $permanent; - - /** - * @param ClientInterface $client An HTTP adapter - * @param string $accessToken Your Mapbox access token - * @param string|null $country Restrict results to one or more ISO 3166 alpha-2 countries (comma separated) - * @param bool $permanent Store results permanently (v6 `permanent` parameter) - */ - public function __construct( - ClientInterface $client, - string $accessToken, - ?string $country = null, - bool $permanent = false, - ) { - parent::__construct($client); - - $this->accessToken = $accessToken; - $this->country = $country; - $this->permanent = $permanent; - } - - public function geocodeQuery(GeocodeQuery $query): Collection - { - // The Mapbox API does not geocode raw IP addresses - if (filter_var($query->getText(), FILTER_VALIDATE_IP)) { - throw new UnsupportedOperation('The Mapbox provider does not support IP addresses, only street addresses.'); - } - - $parameters = []; - - // v6 Structured Input: typed fields replace the `q` search text - $structured = $this->structuredInputFields($query); - if ([] !== $structured) { - foreach (self::STRUCTURED_INPUT_FIELDS as $field) { - if (isset($structured[$field])) { - $parameters[$field] = $structured[$field]; - } - } - } else { - $parameters['q'] = $query->getText(); - } - - if ($query->getBounds()) { - // Format is "minLon,minLat,maxLon,maxLat" - $parameters['bbox'] = sprintf( - '%s,%s,%s,%s', - $query->getBounds()->getWest(), - $query->getBounds()->getSouth(), - $query->getBounds()->getEast(), - $query->getBounds()->getNorth() - ); - } - - $parameters['types'] = $this->locationTypes($query); - - if (null !== $autocomplete = $query->getData('autocomplete')) { - $parameters['autocomplete'] = $autocomplete ? 'true' : 'false'; - } elseif ([] !== $structured) { - // Mapbox recommends autocomplete=false for structured input - $parameters['autocomplete'] = 'false'; - } - - if (null !== $proximity = $query->getData('proximity')) { - $parameters['proximity'] = $proximity; - } - - if (null !== $worldview = $query->getData('worldview')) { - $parameters['worldview'] = $worldview; - } - - return $this->fetchUrl( - self::FORWARD_ENDPOINT_URL, - $query->getLimit(), - $query->getLocale(), - $query->getData('country', $this->country), - $parameters - ); - } - - public function reverseQuery(ReverseQuery $query): Collection - { - $coordinate = $query->getCoordinates(); - $parameters = [ - 'longitude' => $coordinate->getLongitude(), - 'latitude' => $coordinate->getLatitude(), - ]; - - $parameters['types'] = $this->locationTypes($query); - - return $this->fetchUrl( - self::REVERSE_ENDPOINT_URL, - $query->getLimit(), - $query->getLocale(), - $query->getData('country', $this->country), - $parameters - ); - } - - /** - * @return array - */ - 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; - } - - /** - * @param GeocodeQuery|ReverseQuery $query - */ - private function locationTypes($query): string - { - if (null !== $locationType = $query->getData('location_type')) { - return is_array($locationType) ? implode(',', $locationType) : $locationType; - } - - return self::DEFAULT_TYPE; - } -``` - -**Step 6.4: Replace `buildQuery`** - -```php - /** - * @param array $parameters - */ - private function buildQuery(string $url, int $limit, ?string $locale, ?string $country, array $parameters): string - { - if (null !== $country) { - $parameters['country'] = $country; - } - - if (null !== $locale) { - $parameters['language'] = $locale; - } - - $parameters['limit'] = $limit; - - if ($this->permanent) { - $parameters['permanent'] = 'true'; - } - - $parameters['access_token'] = $this->accessToken; - - return $url.'?'.http_build_query($parameters); - } -``` - -(Parameter insertion order is part of the observable URL — see design doc; the -integration fixtures in Task 0 depend on it.) - -**Step 6.5: Adapt `fetchUrl`'s signature** (keep v5 body for now; parsing changes in Task 7) - -```php - /** - * @param array $parameters - */ - private function fetchUrl(string $url, int $limit, ?string $locale, ?string $country, array $parameters): AddressCollection - { - $url = $this->buildQuery($url, $limit, $locale, $country, $parameters); - $content = $this->getUrlContents($url); - // ... existing v5 body stays until Task 7 - } -``` - -**Step 6.6: Run the URL tests to verify they pass (GREEN)** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php --filter "Url"` -Expected: PASS (6 URL tests). Parsing + integration tests still fail (expected). - -**Step 6.7: Commit** - -```bash -git add src/Provider/Mapbox/Mapbox.php -git commit -m "feat(mapbox): build requests for the Geocoding API v6" -``` - ---- - -### Task 7: Implement v6 response parsing (GREEN for parsing tests) - -**Files:** -- Modify: `src/Provider/Mapbox/Mapbox.php` - -**Step 7.1: Replace the parsing section of `fetchUrl` and the helpers** - -`validateResponse(string $url, string $content): array` and -`parseCoordinates(AddressBuilder $builder, array $result): void` stay unchanged from v5 -(`geometry.coordinates` = [lon, lat] → `setCoordinates(lat, lon)`; feature `bbox` -[minLon, minLat, maxLon, maxLat] → `setBounds(south, west, north, east)` when present — -documented for non-address features but not currently returned; map defensively). - -Replace `updateAddressComponent` and the feature loop with: - -```php - private function fetchUrl(string $url, int $limit, ?string $locale, ?string $country, array $parameters): AddressCollection - { - $url = $this->buildQuery($url, $limit, $locale, $country, $parameters); - $content = $this->getUrlContents($url); - $json = $this->validateResponse($url, $content); - - // no result - if (!isset($json['features']) || !count($json['features'])) { - return new AddressCollection([]); - } - - $results = []; - foreach ($json['features'] as $result) { - $results[] = $this->parseFeature($result); - - if (count($results) >= $limit) { - break; - } - } - - return new AddressCollection($results); - } - - /** - * @param array $result - */ - private function parseFeature(array $result): MapboxAddress - { - $properties = $result['properties'] ?? []; - $context = $properties['context'] ?? []; - - $builder = new AddressBuilder($this->getName()); - $this->parseCoordinates($builder, $result); - - // update address components (context is an object keyed by feature type in v6) - foreach ($context as $type => $component) { - if (is_array($component)) { - $this->updateAddressComponent($builder, (string) $type, $component); - } - } - - $address = $builder->build(MapboxAddress::class); - - if (isset($properties['mapbox_id'])) { - $address = $address->withId((string) $properties['mapbox_id']); - } elseif (isset($result['id'])) { - $address = $address->withId((string) $result['id']); - } - - // street name without the house number (v5 semantics) - $streetName = $context['address']['street_name'] ?? $context['street']['name'] ?? $properties['name'] ?? null; - if (null !== $streetName) { - $address = $address->withStreetName((string) $streetName); - } - - $streetNumber = $context['address']['address_number'] ?? null; - if (null !== $streetNumber) { - $address = $address->withStreetNumber((string) $streetNumber); - } - - if (isset($properties['feature_type'])) { - $address = $address->withResultType([(string) $properties['feature_type']]); - } - - if (isset($properties['full_address'])) { - $address = $address->withFormattedAddress((string) $properties['full_address']); - } - - $neighborhood = $context['neighborhood']['name'] ?? null; - if (null !== $neighborhood && '' !== $neighborhood) { - $address = $address->withNeighborhood((string) $neighborhood); - } - - if (isset($properties['match_code']) && is_array($properties['match_code'])) { - $address = $address->withMatchCode($properties['match_code']); - $confidence = $properties['match_code']['confidence'] ?? null; - if (null !== $confidence) { - $address = $address->withMatchConfidence((string) $confidence); - } - } - - if (isset($properties['coordinates']['accuracy'])) { - $address = $address->withAccuracy((string) $properties['coordinates']['accuracy']); - } - - return $address; - } - - /** - * Update current resultSet with given key/value. - * - * @param array $component - */ - private function updateAddressComponent(AddressBuilder $builder, string $type, array $component): void - { - if (!isset($component['name']) || '' === $component['name']) { - return; - } - - switch ($type) { - case 'postcode': - $builder->setPostalCode($component['name']); - - break; - - case 'locality': - $builder->setLocality($component['name']); - - break; - - case 'country': - $builder->setCountry($component['name']); - if (isset($component['country_code'])) { - $builder->setCountryCode(strtoupper((string) $component['country_code'])); - } - - break; - - case 'place': - $builder->addAdminLevel(1, $component['name']); - $builder->setLocality($component['name']); - - break; - - case 'region': - $code = isset($component['region_code']) ? (string) $component['region_code'] : null; - $builder->addAdminLevel(2, $component['name'], $code); - - break; - - default: - // address, street, district, neighborhood: handled (or intentionally ignored) elsewhere - } - } -``` - -Notes: -- Context iteration order is the JSON key order; later entries win for `locality` - (`locality` then `place`), matching the v5 precedence. -- `district` is intentionally ignored (same as v5). -- Remove the now-unused `use Geocoder\Exception\InvalidArgument;` import if it became - unused (the mode-validation is gone). - -**Step 7.2: Run the parsing tests to verify they pass (GREEN)** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php --filter "Parse"` -Expected: PASS (6 parsing tests). - -**Step 7.3: Run the whole Mapbox suite** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` -Expected: unit tests all PASS; the 7 integration tests still FAIL (see Task 8). - -**Step 7.4: Commit** - -```bash -git add src/Provider/Mapbox/Mapbox.php -git commit -m "feat(mapbox): parse Geocoding API v6 responses" -``` - ---- - -### Task 8: Integration tests green (full Mapbox suite green) - -**Files:** none (verification only — if anything fails, fix the IMPLEMENTATION, re-run, -and keep going; do not edit test expectations to match broken output) - -**Step 8.1: Run the integration tests** - -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php --filter "testGeocodeWith|testReverseWith"` -Expected: all 7 integration tests PASS (fixtures replayed from `.cached_responses`). - -If a test fails with `InvalidCredentials`: the provider built a URL different from the -recorded one — diff the expected URL (Task 0 list / design doc parameter order) against the -actual. `CachedResponseClient` records a NEW file on cache miss, so a stray file appearing -in `.cached_responses` means a URL mismatch — delete the stray file and fix the provider. - -**Step 8.2: Run the full Mapbox suite + model tests** - -Run: -```bash -vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php -vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxAddressTest.php -``` -Expected: ALL PASS (20 + 3 tests), no warnings or deprecations in the output. - -**Step 8.3: Commit** (nothing to commit if no changes — skip this step in that case) - ---- - -### Task 9: Cleanup, CHANGELOG, docs, full verification - -**Files:** -- Delete: stale files in `src/Provider/Mapbox/Tests/.cached_responses/` (everything except the 7 Task-0 fixtures) -- Modify: `src/Provider/Mapbox/CHANGELOG.md` -- Modify: `src/Provider/Mapbox/Readme.md` -- Modify: `README.md` (main, line ~137) - -**Step 9.1: Remove stale cache files** (keep only the 7 fixtures from Task 0) - -```bash -php -r ' -$keep = [ - "5e53474a50aa75267953e6d90b2e3472021efbf2", - "fee388586d54f2150af453c6e033e452d85e46a8", - "7b8b47952b295102acd4cb1bc0a9536c26c7bc9b", - "d73a2016dbd70a152bffadf02f4f04d7e4d6a0b7", - "338a05e2ceeb9a75e55774746945065683127870", - "9e3e5555aca6117bf14dbd86c37ed23cbece335d", - "06551ee827f3b9635793737514ec6b9d368075c4", -]; -$dir = "src/Provider/Mapbox/Tests/.cached_responses"; -foreach (glob("$dir/api.mapbox.com_*") as $f) { - if (!in_array(substr(basename($f), 14), $keep, true)) { - unlink($f); - echo "removed ", basename($f), PHP_EOL; - } -} -' -``` -Expected: 10 files removed (7 old v5 fixtures — 3 of them already deleted in the working -tree — plus 3 v6 zero-result probe files). Re-run the Mapbox suite to confirm the cleanup -broke nothing: -Run: `vendor/bin/phpunit src/Provider/Mapbox/Tests/MapboxTest.php` -Expected: ALL PASS. - -**Step 9.2: Provider CHANGELOG** — prepend to `src/Provider/Mapbox/CHANGELOG.md` (after the -header, before `## 1.5.0`): - -```markdown -## 2.0.0 - -### Added - -- Support for the Mapbox Geocoding API v6 (`/search/geocode/v6/forward` and `/search/geocode/v6/reverse`) -- v6 Structured Input: forward queries built from typed address fields via query data — `address_line1`, `address_number`, `street`, `block`, `place`, `region`, `postcode`, `locality`, `neighborhood`. When any of these is present, the query text is not sent. -- `Mapbox::TYPE_STREET` (new v6 type, added to `Mapbox::TYPES`) -- `Mapbox::TYPE_BLOCK` (Japan) and `Mapbox::TYPE_SECONDARY_ADDRESS` (US) constants (not part of `TYPES`: the API only accepts them in the corresponding regional context) -- `Mapbox::STRUCTURED_INPUT_FIELDS` constant -- Query data support: `autocomplete` (replaces `fuzzy_match`), `proximity` (`"lon,lat"` or `"ip"`), `worldview` -- New 4th constructor argument `bool $permanent` (v6 `permanent=true` result storage) -- `MapboxAddress::getMatchCode()` / `withMatchCode()` — v6 Smart Address Match object -- `MapboxAddress::getMatchConfidence()` / `withMatchConfidence()` — `exact`, `high`, `medium` or `low` -- `MapboxAddress::getAccuracy()` / `withAccuracy()` — `rooftop`, `parcel`, `point`, `interpolated`, `approximate` or `intersection` - -### Removed - -- Support for the Mapbox Geocoding API v5 (`/geocoding/v5/...`) — pin to `geocoder-php/mapbox-provider:^1.5` to stay on v5 -- `Mapbox::GEOCODE_ENDPOINT_URL_SSL` and `Mapbox::REVERSE_ENDPOINT_URL_SSL` constants (replaced by `Mapbox::FORWARD_ENDPOINT_URL` / `Mapbox::REVERSE_ENDPOINT_URL`) -- `Mapbox::GEOCODING_MODE_PLACES`, `Mapbox::GEOCODING_MODE_PLACES_PERMANENT` and `Mapbox::GEOCODING_MODES` constants (v6 has no geocoding modes; use the `permanent` constructor argument) -- `Mapbox::TYPE_POI` and `Mapbox::TYPE_POI_LANDMARK` constants (v6 does not geocode POIs; passing them as `location_type` now yields a 422 error from the API) -- `fuzzy_match` query data (v6 has no fuzzy/typo matching; use `autocomplete` for prefix-style matching) - -### Changed - -- The 4th constructor argument is now `bool $permanent = false` (was `string $geocodingMode`); passing a mode string changes behavior or errors -- `Mapbox::TYPES` no longer contains the POI types and now contains `street` -- Forward requests send the search text as the `q` query parameter instead of in the URL path -- Reverse requests send `longitude` / `latitude` query parameters (was `{lon},{lat}` in the path) -- `MapboxAddress::getId()` returns the v6 `mapbox_id` value (format changed, e.g. `dXJu...` instead of `address.123`) -- `MapboxAddress::getResultType()` values follow the v6 `feature_type` vocabulary (`street` added, `poi`/`poi.landmark` gone) -- Reverse geocoding with a `limit` greater than 1 requires a single `location_type` value (v6 API constraint: multiple types + limit returns a 422 error) -- Forward `limit` is capped at 10 by the v6 API (higher values are silently clamped) -- Invalid access tokens are now answered with HTTP 401 by the API (v5 used 403); both map to `Geocoder\Exception\InvalidCredentials` -- Geocoded data reflects the v6 dataset: coordinates, results and admin-level codes may differ from v5 -``` - -**Step 9.3: Provider README** — in `src/Provider/Mapbox/Readme.md`, after the `### Note` -paragraph, insert: - -```markdown -### Usage - -This provider targets the **Mapbox Geocoding API v6**. If you rely on the v5 API, pin -`geocoder-php/mapbox-provider` to `^1.5`. - -```php -$provider = new Mapbox($client, $accessToken); -// optional: country filter (ISO 3166 alpha-2) and permanent result storage -$provider = new Mapbox($client, $accessToken, 'US', true); - -// Forward geocoding -$provider->geocodeQuery(GeocodeQuery::create('149 9th St, San Francisco, CA 94103')); - -// Forward geocoding with options (query data) -$query = GeocodeQuery::create('washi') - ->withData('autocomplete', true) // prefix-style matching (on by default) - ->withData('location_type', Mapbox::TYPE_STREET) // or an array of Mapbox::TYPE_* constants - ->withData('proximity', '-120.09,47.60') // or 'ip' - ->withData('worldview', 'us') - ->withData('country', 'US'); - -// v6 Structured Input: typed fields replace the free-text query (the text is not sent) -$query = GeocodeQuery::create('2595 Lucky John Dr, Park City, UT 84060') - ->withData('address_number', '2595') - ->withData('street', 'Lucky John Dr') - ->withData('place', 'Park City') - ->withData('region', 'UT') - ->withData('postcode', '84060'); -// Any of address_line1, address_number, street, block, place, region, postcode, -// locality, neighborhood triggers Structured Input; autocomplete defaults to false. - -// Reverse geocoding -$provider->reverseQuery(ReverseQuery::fromCoordinates(48.8631507, 2.388911)); -``` - -Results are `Geocoder\Provider\Mapbox\Model\MapboxAddress` instances. Besides the regular -address data they expose `getId()`, `getStreetName()`, `getStreetNumber()`, -`getResultType()`, `getFormattedAddress()`, `getNeighborhood()`, -`getMatchCode()` / `getMatchConfidence()` (v6 Smart Address Match) and `getAccuracy()`. -``` - -(Note: the README contains nested code fences — when editing, keep the existing file's -fence style so the Markdown renders correctly.) - -**Step 9.4: Main README** — update the Mapbox row (line ~137) website link: - -``` -[Website](https://www.mapbox.com/geocoding/) → [Website](https://docs.mapbox.com/api/search/geocoding/) -``` - -**Step 9.5: Commit** - -```bash -git add -A src/Provider/Mapbox README.md -git commit -m "chore(mapbox): drop v5 fixtures; docs(mapbox): document v6 upgrade (2.0.0)" -``` - -**Step 9.6: Full verification (CONTRIBUTING.md: "You MUST run the test suite")** - -Run, in order — all must pass with pristine output: -```bash -composer test # full suite, all providers -composer analyse # PHPStan level 6 -composer cs # PHP-CS-Fixer dry-run (if it reports fixes, run composer cs:fix, re-run composer test, and amend/append a commit) -``` - -Fix any PHPStan/CS findings in the touched files (Mapbox.php, MapboxAddress.php, tests) -and re-run until all three commands are clean. - ---- - -## Final state checklist - -- [ ] `Geocoder\Provider\Mapbox\Mapbox` builds v6 forward/reverse/structured requests (deterministic parameter order) -- [ ] v6 responses parsed onto `MapboxAddress` (+ `matchCode`, `matchConfidence`, `accuracy`) -- [ ] `TYPE_POI` / `TYPE_POI_LANDMARK` / `GEOCODING_MODE_*` removed; `TYPE_STREET` / `TYPE_BLOCK` / `TYPE_SECONDARY_ADDRESS` added; `TYPES` updated -- [ ] Constructor 4th arg is `bool $permanent` -- [ ] `fuzzy_match` gone, `autocomplete` / `proximity` / `worldview` supported -- [ ] 7 v6 integration fixtures replayed in CI with the placeholder key; all v5 fixtures deleted -- [ ] Provider CHANGELOG 2.0.0 lists every breaking change -- [ ] Provider README documents v6 usage incl. Structured Input; main README link updated -- [ ] `composer test` / `composer analyse` / `composer cs` all pass -- [ ] Commits are small, meaningful, in TDD order (tests before implementation) From 2c8ae2711e424d732c5d41a610df72fac8b390bb Mon Sep 17 00:00:00 2001 From: David Barratt Date: Sat, 29 Aug 2026 09:33:31 -0400 Subject: [PATCH 13/13] CS Fixes --- src/Provider/IP2Location/IP2Location.php | 3 +-- src/Provider/MapQuest/MapQuest.php | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Provider/IP2Location/IP2Location.php b/src/Provider/IP2Location/IP2Location.php index 111401003..28972ce1c 100644 --- a/src/Provider/IP2Location/IP2Location.php +++ b/src/Provider/IP2Location/IP2Location.php @@ -102,9 +102,8 @@ private function executeQuery(string $url): AddressCollection throw new InvalidCredentials('Insufficient credits to use IP2Location service.'); } elseif (preg_match('/invalid ip address/i', $data['response'])) { throw new UnsupportedOperation('Invalid IP address.'); - } else { - throw new UnsupportedOperation(sprintf('Unexpected error: %s.', $data['response'])); } + throw new UnsupportedOperation(sprintf('Unexpected error: %s.', $data['response'])); } if (isset($data['region_name'])) { diff --git a/src/Provider/MapQuest/MapQuest.php b/src/Provider/MapQuest/MapQuest.php index 4c433467e..d3dae33db 100644 --- a/src/Provider/MapQuest/MapQuest.php +++ b/src/Provider/MapQuest/MapQuest.php @@ -157,11 +157,10 @@ public function geocodeQuery(GeocodeQuery $query): Collection $params = $this->addOptionsForGetQuery($params, $options); return $this->executeGetQuery(static::GEOCODE_ENDPOINT, $params); - } else { - $params = $this->addOptionsForPostQuery($params, $options); - - return $this->executePostQuery(static::GEOCODE_ENDPOINT, $params); } + $params = $this->addOptionsForPostQuery($params, $options); + + return $this->executePostQuery(static::GEOCODE_ENDPOINT, $params); } public function reverseQuery(ReverseQuery $query): Collection