# geography — provinces, cities, districts > Client seam `client/src/services/geography/` · `USE_GEOGRAPHY_MOCK = false` (**real**) · 13 server ops > Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29). The reference hierarchy every address, service area and search filter is keyed on. Also the home of the single most load-bearing null in the schema. ## Endpoints | Method | Path | Auth | Verdict | | --- | --- | --- | --- | | GET | `/api/v1/geo/provinces` | **anonymous** | wired | | GET | `/api/v1/geo/cities` | **anonymous** | wired | | GET | `/api/v1/geo/districts` | **anonymous** | wired | | GET | `/api/v1/geo/tree` | **anonymous** | **unwired** — the client fetches the three levels separately and caches each | | POST | `/api/v1/admin_geo/create_province` | admin | **unwired** — no console screen | | POST | `/api/v1/admin_geo/update_province/{id}` | admin | **unwired** | | POST | `/api/v1/admin_geo/set_province_active/{id}` | admin | **unwired** | | POST | `/api/v1/admin_geo/create_city` | admin | **unwired** | | POST | `/api/v1/admin_geo/update_city/{id}` | admin | **unwired** | | POST | `/api/v1/admin_geo/set_city_active/{id}` | admin | **unwired** | | POST | `/api/v1/admin_geo/create_district` | admin | **unwired** | | POST | `/api/v1/admin_geo/update_district/{id}` | admin | **unwired** | | POST | `/api/v1/admin_geo/set_district_active/{id}` | admin | **unwired** | No phantoms. The nine `admin_geo` routes are `DynamicPermission` + `sensitive`; geography is seeded, so there is no console editor. Mutations are **action-style** (`admin_geo/create_city`, never `admin_geo/cities`). ## `districtId = null` means whole-city This is the one rule to get right, and it reads in **both** directions: - **On a nurse service area** (see [service-areas.md](service-areas.md)), `districtId = null` means the nurse covers the **entire city**, not "no district". - **On a search query**, a customer in district *D* must match both a nurse whose area names *D* and a nurse whose area is whole-city. See [search.md](search.md). - **On an address** it is genuinely optional metadata — a missing district does not widen anything. Never coerce the null to 0 or to a sentinel id, and never write a query that drops whole-city rows. ## Shape rules the JSON does not express - Every level returns **both** `nameFa` and `nameEn`; the client picks by locale. - `isActive` filters the pickers. An inactive city must stay **resolvable** — an existing address or booking references it — so it is filtered from selection, never deleted. - **Two Neshan keys exist and they are different products.** The server's geocoder key is `Seams:Geocoding:ApiKey` (server-side address→point); the client's map key is `NEXT_PUBLIC_NESHAN_KEY` (a *web* key for the embeddable map/search). Never share one value between them. With the client key unset, `AddressMapPicker` falls back to a bounded-canvas grid, which is why dev, CI and jsdom all work without it. See [../config-matrix.md](../config-matrix.md). - Geocoding is a seam: `Seams:Geocoding:Provider` = `mock` (default) or `neshan`. The mock resolves a deterministic point near the city centroid; an address containing `NO_GEO` resolves to null coordinates so the "saved without a map pin" state is testable per-request. ## Enums None. All three levels are integer ids with localised names. ## Open REQs None. REQ-008 (accept the client-picked pin) and REQ-009 (`provinceId` on the address DTO) were delivered and are documented in [addresses.md](addresses.md).