Board: surface the newly-nested municipals in county drill-downs

build_viz.py was resolving officials to counties without the place→county
crosswalk, so only FL's 67 counties had rosters. Factor the resolver out of
build.py into a shared build.place_resolver() and use it in both, so the
Board and the tree agree by construction.

- scripts/build.py — extract place_resolver(); main() now calls it (tree
  output byte-identical, refactor is behavior-preserving)
- scripts/build_viz.py — resolve via the shared resolver
- viz/county_data.json, viz/county_detail.json — regenerated

County drill-downs: 67 → 1049 counties with rosters; officials mapped
5184/5226 (42 county-unresolvable, tree-only). Deterministic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Fabio
2026-07-04 20:36:42 -04:00
parent 199d8b37d3
commit 662ccecb26
4 changed files with 33 additions and 23 deletions
+24 -15
View File
@@ -74,6 +74,29 @@ def acs_cslug(row):
return slugify(norm_county(re.sub(r"\s+County$", "", base)))
def place_resolver(acs_county, crosswalk):
"""(normalized place name, state) -> county dir slug, from the crosswalk.
county_geoid is the stable key; the slug is taken from the ACS county node
(the same source that names county dirs) so municipals nest correctly. Only
unique name matches whose county has an ACS node are included — ambiguous
names, label-less rows, and counties with no ACS node fall through.
Shared by build.py and build_viz.py so the tree and the Board agree.
"""
fips_to_cslug = {canonical_fips(r["county_fips"]): acs_cslug(r) for r in acs_county}
geoids = {}
for x in crosswalk:
geoids.setdefault(
(norm_place(x["place_name"]), x["state_code"]), set()).add(x["county_geoid"])
out = {}
for key, geos in geoids.items():
if len(geos) == 1:
cf = canonical_fips(next(iter(geos)))
if cf in fips_to_cslug:
out[key] = fips_to_cslug[cf]
return out
# committee_title (source) -> normalized role on the committee
ROLE_MAP = {
"member": "member", "Chair": "chair", "Chairman": "chair",
@@ -572,21 +595,7 @@ def main():
acs_county = load("acs_county.jsonl")
acs_cd = load("acs_cd.jsonl")
crosswalk = load("place_county_crosswalk.jsonl")
# place -> county resolver: (normalized place name, state) -> county dir slug.
# county_geoid is the stable key; the slug is taken from the ACS county node
# (the same source that names county dirs) so municipals nest correctly.
fips_to_cslug = {canonical_fips(r["county_fips"]): acs_cslug(r) for r in acs_county}
_place_geoids = {}
for x in crosswalk:
_place_geoids.setdefault(
(norm_place(x["place_name"]), x["state_code"]), set()).add(x["county_geoid"])
place_to_cslug = {}
for key, geos in _place_geoids.items():
if len(geos) == 1:
cf = canonical_fips(next(iter(geos)))
if cf in fips_to_cslug:
place_to_cslug[key] = fips_to_cslug[cf]
place_to_cslug = place_resolver(acs_county, crosswalk)
bodies_by_code = {b["code"]: b for b in bodies}
comm_codes = committee_codes(bodies)