Spaces:
Running
Running
| # 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_<county>()` 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 (`<county>.gov/gis`, `data.<county>...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_<county>(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. | |