# Domain 2 — Geographic Data [← Database Model](index.md) ### `provinces` / `cities` / `districts` [CORE]/[MVP] **Role:** The geo hierarchy backing service areas, addresses, and search. **Why a table, not a static list:** new cities/districts launch without a deploy, and `sort_order`/`is_active` drive ordered, toggleable dropdowns. `districts` map to Tehran's 22 municipal districts or major neighborhoods elsewhere; they are **optional** (a nurse can cover a whole city). Fields unchanged. **Relations:** `provinces` 1:N `cities` 1:N `districts`; referenced by `customer_addresses` and `nurse_service_areas`. ### `nurse_service_areas` [CORE] **Role:** Where a nurse will travel. A row with `district_id = NULL` means the entire city. **Why a join table (not a radius):** Iranian nurses think in named districts, not GPS radii; this also drives the geographic filter in search cheaply. Unchanged, with `UNIQUE(nurse_id, city_id, district_id)`. **Relations:** N:1 → `nurse_profiles`, `cities`, `districts`. > **As-built (backend-phase-4):** the geo hierarchy lives in a **`geo` schema** (`Provinces`/`Cities`/ > `Districts`/`NurseServiceAreas`), seeded via `HasData` with all 31 provinces, each province's capital > city, and Tehran's 22 مناطق. Because SQL Server treats NULLs as distinct in a unique index, the > whole-city (`district_id = NULL`) uniqueness is enforced with a **filtered-index pair** — > `UNIQUE(nurse_id, city_id) WHERE district_id IS NULL AND deleted_at IS NULL` **plus** > `UNIQUE(nurse_id, city_id, district_id) WHERE district_id IS NOT NULL AND deleted_at IS NULL` — so both a > duplicate whole-city row and a duplicate city+district row are rejected (surfaced as `409`), while a > soft-removed area can be re-declared. Public lookups are cached and filter `is_active` at every level > (a deactivated parent hides its children); geocoding is behind the mocked **`IGeocoder`** seam.