Skip to content

Commit e67a4c1

Browse files
committed
docs: add design and implementation plan for Mapbox Geocoding v6
1 parent d8ac0a5 commit e67a4c1

2 files changed

Lines changed: 1956 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Mapbox Geocoding v6 — Design
2+
3+
Issue: https://github.com/geocoder-php/Geocoder/issues/1237
4+
Date: 2026-08-28
5+
Status: validated
6+
7+
## Background
8+
9+
Mapbox released the [Geocoding API v6](https://docs.mapbox.com/api/search/geocoding/) (GA). The v6
10+
API is more precise, adds **Structured Input** (typed address fields instead of free text),
11+
**Smart Address Match** (`match_code`), point `accuracy`, `routable_points`, and new feature
12+
types. The v5 API keeps running; Mapbox does not force a migration.
13+
14+
## Decision
15+
16+
**Upgrade the existing `Geocoder\Provider\Mapbox\Mapbox` provider in place, as a major
17+
version of `geocoder-php/mapbox-provider` (1.x → 2.0.0).** All breaking changes are listed
18+
in the provider CHANGELOG. Users who need v5 pin to `^1.5`.
19+
20+
Rationale: the issue asks for *the* Mapbox provider to support v6; the repo's deprecation
21+
procedure (`docs/procedures/provider-deprecation.md`) only applies once an API stops
22+
responding, which has not happened for v5 — but the provider may move forward in a major
23+
release. (A parallel `MapboxV6` class was considered and rejected in favor of a clean
24+
major bump.)
25+
26+
## Verified v6 API behavior (probed against the live API)
27+
28+
- Forward: `GET https://api.mapbox.com/search/geocode/v6/forward?q={text}` (or structured fields, no `q`)
29+
- Reverse: `GET https://api.mapbox.com/search/geocode/v6/reverse?longitude={lon}&latitude={lat}`
30+
- Invalid token → **401** (v5 used 403). Both map to `InvalidCredentials` in `AbstractHttpProvider`.
31+
- `types=poi` / `types=poi.landmark` → **422** (POI geocoding removed in v6).
32+
- `types` valid values (per the API's own 422 message): `country, region, postcode, district,
33+
place, locality, neighborhood, street, block, address, secondary_address`. `block` is only
34+
accepted in Japan context (`country=jp`), `secondary_address` in US context; the docs list
35+
the 9 globally-safe values.
36+
- Forward `limit=11` → 200 (silently clamped to the max of 10).
37+
- Reverse `limit>1` **requires exactly one** `types` value, else 422.
38+
- `autocomplete` (replaces v5 `fuzzyMatch`, inverted default): prefix-style matching; a typo'd
39+
query (`wahsington`) returns nothing in v6 — there is no typo tolerance; that is what
40+
`match_code` confidence is for.
41+
- Structured Input: `q` is dropped in favor of `address_line1`, `address_number`, `street`,
42+
`block`, `place`, `region`, `postcode`, `locality`, `neighborhood` (plus `country`); Mapbox
43+
recommends `autocomplete=false` for structured input.
44+
- Response: GeoJSON. `properties` contains `mapbox_id`, `feature_type` (string, formerly
45+
`place_type` array), `name`, `full_address`, `coordinates{longitude,latitude,accuracy,
46+
routable_points}`, `match_code{... ,confidence}` (forward only) and `context` as an
47+
**object keyed by feature type** (v5 was an array of `{id,text,short_code}`).
48+
`context.country` uses `country_code`, `context.region` uses `region_code`/`region_code_full`,
49+
`context.address` uses `street_name`/`address_number`.
50+
- `bbox` is documented on non-address features but is **not currently returned** in any of the
51+
probed responses; the parser maps it defensively when present.
52+
53+
## Public API surface (2.0.0)
54+
55+
### Constructor (breaking)
56+
57+
```php
58+
new Mapbox(ClientInterface $client, string $accessToken, ?string $country = null, bool $permanent = false)
59+
```
60+
61+
The 4th argument changes from `string $geocodingMode` to `bool $permanent` (v6 `permanent=true`
62+
storage, applied to forward and reverse requests).
63+
64+
### Constants
65+
66+
- `FORWARD_ENDPOINT_URL`, `REVERSE_ENDPOINT_URL` (replace `GEOCODE_ENDPOINT_URL_SSL` /
67+
`REVERSE_ENDPOINT_URL_SSL`).
68+
- Types: `country, region, postcode, district, place, locality, neighborhood, street, address`
69+
in `TYPES` (adds `TYPE_STREET`; removes `TYPE_POI` / `TYPE_POI_LANDMARK`).
70+
- `TYPE_BLOCK` (Japan) and `TYPE_SECONDARY_ADDRESS` (US) exposed as constants but **not** in
71+
`TYPES` (conditionally accepted by the API only).
72+
- `STRUCTURED_INPUT_FIELDS`: the data keys that trigger Structured Input.
73+
- Removed: `GEOCODING_MODE_PLACES`, `GEOCODING_MODE_PLACES_PERMANENT`, `GEOCODING_MODES`.
74+
75+
### Query data keys
76+
77+
| Key | v6 param | Note |
78+
|---|---|---|
79+
| `location_type` (string\|array) | `types` | kept from v5; default `address` |
80+
| `autocomplete` (bool) | `autocomplete` | replaces `fuzzy_match` (removed) |
81+
| `country` (string) | `country` | kept; also a structured field |
82+
| `proximity` (`"lon,lat"`\|`"ip"`) | `proximity` | new |
83+
| `worldview` (string) | `worldview` | new |
84+
| `address_line1`, `address_number`, `street`, `block`, `place`, `region`, `postcode`, `locality`, `neighborhood` | same | structured input |
85+
| `bounds` → `bbox`, `locale` → `language`, `limit` → `limit` | | kept |
86+
87+
**Mode selection:** if any structured field is present, `q` is not sent and `autocomplete`
88+
defaults to `false` (unless the user sets it). Otherwise `q` = query text. (The text must
89+
still be non-empty because `GeocodeQuery::create('')` throws; it is simply not sent in
90+
structured mode.)
91+
92+
### Parameter order (deterministic URLs)
93+
94+
- Forward: `q` | structured fields (canonical order) → `bbox` → `types` → `autocomplete` →
95+
`proximity` → `worldview` → `country` → `language` → `limit` → `permanent` (if true) →
96+
`access_token`
97+
- Reverse: `longitude` → `latitude` → `types` → `country` → `language` → `limit` →
98+
`permanent` (if true) → `access_token`
99+
100+
### Response → model mapping
101+
102+
| Model | v6 source |
103+
|---|---|
104+
| `id` | `properties.mapbox_id` (fallback: feature `id`) |
105+
| `streetName` | `context.address.street_name` → `context.street.name` → `properties.name` (keeps v5 semantics: no house number) |
106+
| `streetNumber` | `context.address.address_number` |
107+
| `formattedAddress` | `properties.full_address` |
108+
| `neighborhood` | `context.neighborhood.name` |
109+
| `resultType` | `[properties.feature_type]` (array kept for BC) |
110+
| `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) |
111+
| `bounds` | feature `bbox` (when present) |
112+
| **new** `matchCode` / `matchConfidence` | `properties.match_code` / `.confidence` |
113+
| **new** `accuracy` | `properties.coordinates.accuracy` |
114+
115+
Errors/behavior: IP text → `UnsupportedOperation` (unchanged); 401/403 → `InvalidCredentials`,
116+
429 → `QuotaExceeded`, other ≥300 → `InvalidServerResponse` (base class, unchanged);
117+
`getName()` stays `mapbox`; client-side result limit kept.
118+
119+
## Testing strategy (TDD)
120+
121+
1. **Mocked unit tests** — no network:
122+
- basics: name, IP rejection ×3, 429, 401, empty reverse response;
123+
- exact-URL assertions via `getMockedHttpClientCallback` (forward basic / all options /
124+
structured / explicit autocomplete override / reverse / reverse with types+country);
125+
- response-parsing tests with inline JSON fixtures (address feature, place feature, street
126+
feature, feature without context, empty features, invalid JSON).
127+
2. **Integration tests with cached responses** (repo convention): 7 real v6 queries recorded
128+
with a real key (see implementation plan for the exact URLs); `CachedResponseClient`
129+
masks the key as `[apikey]` so CI replays the fixtures with the placeholder key.
130+
v5 fixtures are deleted.
131+
3. `composer test` (full suite), `composer analyse` (PHPStan L6), `composer cs` — per
132+
`CONTRIBUTING.md`.
133+
134+
## Out of scope (YAGNI)
135+
136+
- Batch geocoding endpoint (`/v6/batch`)
137+
- `entrances` (public preview)
138+
- `routable_points` on the model
139+
- `format=v5` compatibility mode (we parse native v6)
140+
- Deprecation of the v5 API (it still works; no maintainer decision to deprecate)

0 commit comments

Comments
 (0)