# Adding a New Region (County) The estimator is region-agnostic: each area you cover is one `RegionConfig` in `src/lawn_estimator/regions.py` that wires concrete data-source adapters behind the ports in `src/lawn_estimator/sources/base.py`. Adding a county is finding a handful of public endpoints and writing a ~20-line `build_()` function — no core pipeline changes. Resolution tries each registered region's geocoder in order; the first hit pins that region for the whole run (falling back to validated Google + parcel-pinning if no county address layer matches). --- ## What to gather ### Required (4) | # | Source | What it is / how to find it | Used for | |---|--------|------------------------------|----------| | 1 | **Address points** | An Esri `Address_Points`-style FeatureServer/MapServer layer with a full-address field (usually `FULLADDR`). Search the county's ArcGIS portal (`.gov/gis`, `data....opendata.arcgis.com`). | Free, keyless geocoding that lands *on the building* (so the parcel lookup always hits). Optional — a region can geocode via Google only, but that costs API calls and is less precise. | | 2 | **Parcels** | An Esri parcel FeatureServer (polygon) supporting point-in-polygon queries (`Query` capability). Plus a **field map**: which attribute is the parcel id, the site address, and year-built (if any). | The legal boundary + the year-built vintage guard. | | 3 | **LiDAR** | Almost always **USGS via The National Map** — national coverage. Query `tnmaccess.nationalmap.gov/api/v1/products` (`datasets=Lidar Point Cloud (LPC)`, a bbox over the area) and note the dataset title prefix + flight year. A local county LiDAR bucket only if it's newer/denser (e.g. Douglas's 2022). | Ground points → the high-confidence estimate (with imagery-only fallback when absent). | | 4 | **Local CRS** | The correct UTM zone (EPSG). Eastern Nebraska = `EPSG:26914` (14N); western Iowa / across the Missouri = `EPSG:26915` (15N). | Area math + LiDAR/point projection. | ### Optional (graceful fallback if absent) | # | Source | Absent → fallback | |---|--------|-------------------| | 5 | **Street / road centerlines** (Esri polyline layer) | Street-aware right-of-way extension (Phase 3.5). Absent → extend-all + neighbor subtraction (per-extension sizes still reported so the user can hand-correct). | | 6 | **Ortho imagery** tile service (county aerials) | Crisp display layer. Absent → visualizations render on Google satellite. | --- ## The two counties we ship today (worked examples) **Douglas County, NE** (`build_douglas`) - Address points: `dcgis.org/server/rest/services/vector/Address_Points/FeatureServer/0` (`FULLADDR`) - Parcels: `dcgis.org/server/rest/services/vector/Parcels_public/FeatureServer/0` — field map `{object_id: OBJECTID, site_address: PROPERTY_A, year_built: BLDG_YRBLT, bldg_sqft: BLDG_SF}` - LiDAR: county 2022 QL1 on public S3 (`DouglasS3LidarSource`, vintage 2022, ~8 pts/m²) - Streets: `.../vector/Street_Centerlines/FeatureServer/0` · Ortho: 2025 county tiles · CRS: 26914 **Sarpy County, NE** (`build_sarpy`) - Address points: `geodata.sarpy.gov/arcgis/rest/services/Cadastral/LandRecordsDynamic/MapServer/2` (`FULLADDR`) - Parcels: `services.arcgis.com/OiG7dbwhQEWoy77N/.../Sarpy_Parcels_WFL1/FeatureServer/0` — field map `{object_id: OBJECTID, site_address: SITEADDRESS}` (**no year-built field** → new construction rides the RGB-only fallback instead of a vintage warning) - LiDAR: `UsgsTnmLidarSource(["NE_Eastern_Nebraska_UA_LiDAR_2016"], vintage_year=2016)` (~2 pts/m²) - Streets: `.../LandRecordsDynamic/MapServer/3` (Road Centerlines) · Ortho: none (Google display) · CRS: 26914 Note how the **field map** absorbs schema differences (Douglas `PROPERTY_A` vs Sarpy `SITEADDRESS`) so the pipeline only ever sees canonical keys. --- ## Steps 1. **Find endpoints 1–4** on the county's ArcGIS portal (~30 min). Verify the parcel layer's `?f=json` shows `"capabilities": "Query"` and lists the fields you'll map. 2. **Confirm LiDAR** with a TNM bbox query over a known address; record the dataset prefix + year. (If TNM has nothing, the region still works on RGB-only.) 3. **Write `build_(session, config) -> RegionConfig`** in `regions.py`, reusing `EsriAddressPointsGeocoder`, `EsriParcelSource` (with your field map), `UsgsTnmLidarSource` (or a county source), and optional streets/ortho. 4. **Append it to `REGION_BUILDERS`.** 5. **Validate (the "quality LOOK"):** run 3–5 known addresses. Confirm they resolve to the right region + parcel, LiDAR produces sane ground points (or cleanly falls back), and the visualization aligns (red points on roofs, green on lawn). Spot-check estimates against the RGB vegetation baseline. 6. **Regression:** re-run the Douglas QA batch — it must stay byte-identical (adding a region must never perturb existing ones). --- ## Gotchas learned the hard way - **Gov ArcGIS servers flake** with transient 5xx. All Esri/TNM queries go through `http_get_with_retry`; `resolve_region` degrades to Google on sustained errors. - **Municipality labels** for unincorporated addresses can be the county name; the geocoder prefers the city the user typed. - **Attached housing** (duplex/townhome) has one parcel *per unit* — measuring only the queried address's parcel is correct, not a bug. - **LiDAR vintage is per-source**, not per-region — a newer flight is a one-line swap. - **UTM zone**: crossing the Missouri into Iowa means `EPSG:26915`, not 26914.