File size: 5,565 Bytes
28e0ff1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# 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.