5b0712c31f
Two new entity types, built deterministically from the raw exports:
Candidate (2,494) — 2026 federal candidates as files under
us/states/<st>/candidates/, with office,
district, party, incumbent/challenger stance,
campaign committee, FEC id.
Jurisdiction (3,494) — demographic nodes carrying ACS 2023 data:
3,131 counties (deduped from 3,231 mixed-format
fips keys) + 363 congressional districts.
This completes the county skeleton nationwide: every US county is now a
browsable node with population, income, poverty, race, education and
housing — even where no officeholders are mapped yet. In Florida the
county index.md merges into the existing dir and reports its officeholder
count alongside demographics (St. Lucie: 33 officeholders + ACS profile).
build.py now reads seven raw inputs in one deterministic pass
(byte-identical on rerun). The mirror now answers: who holds power, who
runs this institution, who is running, and what each place is made of.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
679 lines
27 KiB
Python
679 lines
27 KiB
Python
#!/usr/bin/env python3
|
||
"""Build the OKF entity tree from the raw source exports in data/.
|
||
|
||
Inputs (committed raw, one JSON object per line):
|
||
officeholders-v2.jsonl Atlas — person->seat records (11,285)
|
||
bodies.jsonl congress-legislators — institutions (233)
|
||
leadership.jsonl current federal leadership roles (28)
|
||
committee_memberships.jsonl person->committee edges (3,879)
|
||
|
||
Outputs (fully regenerated each run):
|
||
data/jurisdictions/** Person files (federal ones enriched with
|
||
bioguide, leadership, committee seats)
|
||
data/jurisdictions/us/bodies/** Body files (chambers, committees,
|
||
subcommittees) with their leadership
|
||
|
||
Deterministic by construction: sorted iteration, fixed key order, one full
|
||
rebuild per run. Two consecutive runs produce a byte-identical tree.
|
||
"""
|
||
|
||
import json
|
||
import re
|
||
import shutil
|
||
import unicodedata
|
||
from pathlib import Path
|
||
|
||
REPO = Path(__file__).resolve().parent.parent
|
||
DATA = REPO / "data"
|
||
OUT = DATA / "jurisdictions"
|
||
BODIES_OUT = OUT / "us" / "bodies"
|
||
DATASET_DATE = "2026-06-20" # officeholders v2 build date
|
||
CONGRESS_DATE = "2026-07-03" # congress-legislators ingest date
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# helpers
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
def slugify(text):
|
||
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode()
|
||
text = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
|
||
return re.sub(r"-{2,}", "-", text) or "unnamed"
|
||
|
||
|
||
def yval(v):
|
||
if isinstance(v, bool):
|
||
return "true" if v else "false"
|
||
if isinstance(v, (int, float)):
|
||
return str(v)
|
||
return json.dumps(v, ensure_ascii=False)
|
||
|
||
|
||
def norm_name(n):
|
||
"""Normalize a person name for cross-source reconciliation."""
|
||
n = re.sub(r'["\'][^"\']*["\']', "", n) # quoted nickname
|
||
n = re.sub(r",?\s+(Jr|Sr|II|III|IV)\.?(?=\s|$)", "", n) # suffix (any pos)
|
||
return re.sub(r"\s+", " ", n).strip().lower()
|
||
|
||
|
||
def norm_county(name):
|
||
return re.sub(r"\bSaint\b", "St", name)
|
||
|
||
|
||
# committee_title (source) -> normalized role on the committee
|
||
ROLE_MAP = {
|
||
"member": "member", "Chair": "chair", "Chairman": "chair",
|
||
"Chairwoman": "chair", "Cochairman": "co-chair", "Vice Chair": "vice-chair",
|
||
"Vice Chairman": "vice-chair", "Vice Chairwoman": "vice-chair",
|
||
"Ranking Member": "ranking-member", "Ex Officio": "ex-officio",
|
||
}
|
||
LEADER_ROLES = {"chair", "co-chair", "vice-chair", "ranking-member"}
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# body paths / ids
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
INSTITUTIONS = {"house", "senate", "executive"}
|
||
|
||
|
||
def committee_codes(bodies):
|
||
return sorted((b["code"] for b in bodies if b["type"] == "committee"),
|
||
key=len, reverse=True)
|
||
|
||
|
||
def parent_code(sub_code, comm_codes):
|
||
for c in comm_codes:
|
||
if sub_code.startswith(c) and sub_code != c:
|
||
return c
|
||
return None
|
||
|
||
|
||
def body_id(code, bodies_by_code, comm_codes):
|
||
"""Stable path-based id (relative to data/jurisdictions) for a body."""
|
||
b = bodies_by_code[code]
|
||
if b["type"] in INSTITUTIONS:
|
||
return f"us/bodies/{slugify(b['name'])}"
|
||
if b["type"] == "committee":
|
||
return f"us/bodies/{b['chamber']}/committees/{slugify(short_name(b))}"
|
||
parent = parent_code(code, comm_codes)
|
||
pb = bodies_by_code[parent]
|
||
return (f"us/bodies/{pb['chamber']}/committees/{slugify(short_name(pb))}"
|
||
f"/subcommittees/{slugify(short_name(b))}")
|
||
|
||
|
||
def short_name(b):
|
||
"""Committee/subcommittee name minus redundant chamber/parent prefix."""
|
||
name = b["name"]
|
||
name = re.sub(r"^(House|Senate|Joint)\s+(Committee|Subcommittee)\s+on\s+",
|
||
"", name)
|
||
if " - " in name: # subcommittee: keep the tail
|
||
name = name.split(" - ", 1)[1]
|
||
return name
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# federal enrichment index (name -> bioguide, leadership, committee seats)
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
def build_enrichment(leadership, memberships, bodies_by_code, comm_codes):
|
||
idx = {}
|
||
|
||
def slot(name):
|
||
return idx.setdefault(norm_name(name),
|
||
{"bioguide": None, "leadership": [], "committees": []})
|
||
|
||
for l in leadership:
|
||
s = slot(l["person_name"])
|
||
s["bioguide"] = l["bioguide"]
|
||
s["leadership"].append({
|
||
"role": l["role_title"], "body": l["body_code"],
|
||
"since": l.get("start_date"),
|
||
})
|
||
for m in memberships:
|
||
s = slot(m["person_name"])
|
||
s["bioguide"] = m["bioguide"]
|
||
s["committees"].append({
|
||
"code": m["body_code"], "name": m["committee_name"],
|
||
"role": ROLE_MAP.get(m["committee_title"], "member"),
|
||
"rank": m.get("rank"),
|
||
"id": body_id(m["body_code"], bodies_by_code, comm_codes),
|
||
})
|
||
for s in idx.values():
|
||
s["leadership"].sort(key=lambda r: (r["role"], r["body"]))
|
||
s["committees"].sort(key=lambda c: (c["id"]))
|
||
return idx
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Person files
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
def county_slug(rec):
|
||
label = norm_county(rec.get("jurisdiction_label") or "")
|
||
m = re.match(r"(.+?) County, FL$", label)
|
||
if m:
|
||
return slugify(m.group(1))
|
||
if rec["level"] == "municipal":
|
||
m = re.search(r"County:\s*([^;]+)", rec.get("description") or "")
|
||
if m:
|
||
return slugify(norm_county(m.group(1).strip()))
|
||
title = rec.get("title") or ""
|
||
m = re.search(r"\bof\s+(.+?)\s+County\b", title) or re.match(r"(.+?)\s+County\b", title)
|
||
if m:
|
||
return slugify(norm_county(m.group(1)))
|
||
return "_unresolved"
|
||
|
||
|
||
def city_slug(rec):
|
||
m = re.match(r"(.+?), FL$", rec.get("jurisdiction_label") or "")
|
||
if m:
|
||
return slugify(m.group(1))
|
||
m = re.match(r"Mayor of (.+)$", rec.get("title") or "")
|
||
if m:
|
||
return slugify(m.group(1))
|
||
return "_unresolved"
|
||
|
||
|
||
def person_dir(rec):
|
||
lvl, st = rec["level"], (rec.get("state_abbr") or "").lower()
|
||
if lvl == "federal":
|
||
return OUT / "us" / "people"
|
||
if lvl == "state":
|
||
return OUT / "us" / "states" / st / "people"
|
||
if lvl == "county":
|
||
return OUT / "us" / "states" / st / "counties" / county_slug(rec) / "people"
|
||
if lvl == "municipal":
|
||
return (OUT / "us" / "states" / st / "counties" / county_slug(rec)
|
||
/ "municipalities" / city_slug(rec) / "people")
|
||
return OUT / "_unresolved" / "people"
|
||
|
||
|
||
def display_name(rec):
|
||
return rec.get("full_name") or rec.get("title") or "Unknown"
|
||
|
||
|
||
def person_frontmatter(rec, enr):
|
||
lines = ["type: Person", f"title: {yval(display_name(rec))}"]
|
||
jl = rec.get("jurisdiction_label")
|
||
desc = rec.get("title") or ""
|
||
if jl and jl not in desc:
|
||
desc = f"{desc} — {jl}" if desc else jl
|
||
if desc:
|
||
lines.append(f"description: {yval(desc)}")
|
||
if rec.get("title"):
|
||
lines.append(f"role: {yval(rec['title'])}")
|
||
if rec.get("party"):
|
||
lines.append(f"party: {yval(rec['party'])}")
|
||
lines.append(f"level: {yval(rec['level'])}")
|
||
if rec.get("branch"):
|
||
lines.append(f"branch: {yval(rec['branch'])}")
|
||
if rec.get("state_abbr"):
|
||
lines.append(f"state: {yval(rec['state_abbr'])}")
|
||
if rec.get("jurisdiction_type") == "district" and jl:
|
||
lines.append(f"district: {yval(jl)}")
|
||
if enr and enr["leadership"]:
|
||
lines.append("leadership:")
|
||
for r in enr["leadership"]:
|
||
lines.append(f" - role: {yval(r['role'])}")
|
||
lines.append(f" body: {yval(r['body'])}")
|
||
if r.get("since"):
|
||
lines.append(f" since: {yval(r['since'])}")
|
||
if enr and enr["committees"]:
|
||
lines.append("committees:")
|
||
for c in enr["committees"]:
|
||
lines.append(f" - name: {yval(c['name'])}")
|
||
lines.append(f" role: {yval(c['role'])}")
|
||
lines.append(f" body: {yval(c['id'])}")
|
||
contact = [(k, rec.get(k)) for k in ("email", "phone", "website") if rec.get(k)]
|
||
if contact:
|
||
lines.append("contact:")
|
||
for k, v in contact:
|
||
lines.append(f" {k}: {yval(v)}")
|
||
tenure = [(k2, rec.get(k1)) for k1, k2 in
|
||
(("start_date", "start"), ("end_date", "end"),
|
||
("is_current", "current"), ("tenure_notes", "notes"))
|
||
if rec.get(k1) is not None]
|
||
if tenure:
|
||
lines.append("tenure:")
|
||
for k, v in tenure:
|
||
lines.append(f" {k}: {yval(v)}")
|
||
election = [(k2, rec.get(k1)) for k1, k2 in
|
||
(("next_election_year", "next"), ("term_length", "term_length"),
|
||
("term_limit", "term_limit")) if rec.get(k1) is not None]
|
||
if election:
|
||
lines.append("election:")
|
||
for k, v in election:
|
||
lines.append(f" {k}: {yval(v)}")
|
||
lines.append("ids:")
|
||
for k1, k2 in (("person_id", "person"), ("office_id", "office"),
|
||
("tenure_id", "tenure"), ("jurisdiction_id", "jurisdiction")):
|
||
if rec.get(k1):
|
||
lines.append(f" {k2}: {yval(rec[k1])}")
|
||
if enr and enr["bioguide"]:
|
||
lines.append(f" bioguide: {yval(enr['bioguide'])}")
|
||
srcs = [(f, rec[k]) for f, k in (("office", "office_source"),
|
||
("tenure", "tenure_source"), ("jurisdiction", "jurisdiction_source"))
|
||
if rec.get(k)]
|
||
if enr and (enr["leadership"] or enr["committees"]):
|
||
srcs.append(("roles", "congress-legislators (unitedstates project)"))
|
||
if srcs:
|
||
lines.append("sources:")
|
||
for f, s in srcs:
|
||
lines.append(f" - field: {f}")
|
||
lines.append(f" source: {yval(s)}")
|
||
lines.append("confidence: official")
|
||
tags = ["officeholder", rec["level"]]
|
||
if rec.get("branch"):
|
||
tags.append(rec["branch"])
|
||
if rec.get("state_abbr"):
|
||
tags.append(rec["state_abbr"].lower())
|
||
lines.append("tags: [" + ", ".join(tags) + "]")
|
||
lines.append(f"timestamp: {yval(DATASET_DATE)}")
|
||
return lines
|
||
|
||
|
||
def person_body(rec, enr):
|
||
name = display_name(rec)
|
||
role = rec.get("title") or "officeholder"
|
||
jl = rec.get("jurisdiction_label")
|
||
where = f" ({jl})" if jl and jl not in role else ""
|
||
cur = "Current" if rec.get("is_current") else "Former"
|
||
out = [f"# {name}", "", f"{cur} {role}{where}.", ""]
|
||
if enr and enr["leadership"]:
|
||
out += ["## Leadership", ""]
|
||
for r in enr["leadership"]:
|
||
since = f" (since {r['since']})" if r.get("since") else ""
|
||
out.append(f"- {r['role']}{since}")
|
||
out.append("")
|
||
if enr and enr["committees"]:
|
||
out += ["## Committees", ""]
|
||
for c in enr["committees"]:
|
||
tag = "" if c["role"] == "member" else f" — **{c['role']}**"
|
||
out.append(f"- [{c['name']}](/{c['id']}.md){tag}")
|
||
out.append("")
|
||
out += ["## Sources", ""]
|
||
any_src = False
|
||
for f, k in (("office", "office_source"), ("tenure", "tenure_source"),
|
||
("jurisdiction", "jurisdiction_source")):
|
||
if rec.get(k):
|
||
out.append(f"- {f}: {rec[k]}")
|
||
any_src = True
|
||
if enr and (enr["leadership"] or enr["committees"]):
|
||
out.append("- roles: congress-legislators (unitedstates project)")
|
||
any_src = True
|
||
if not any_src:
|
||
out.append("- (no field-level source recorded)")
|
||
out += ["", f"Generated from the Atlas officeholders v2 export ({DATASET_DATE})."]
|
||
return out
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Body files
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
TYPE_LABEL = {"house": "Chamber", "senate": "Chamber", "executive": "Executive Office",
|
||
"committee": "Committee", "subcommittee": "Subcommittee"}
|
||
|
||
|
||
def body_file(b, bid, bodies_by_code, comm_codes, leaders_of, members_count):
|
||
lines = ["type: Body", f"title: {yval(b['name'])}",
|
||
f"classification: {yval(b['type'])}",
|
||
f"chamber: {yval(b['chamber'])}", f"code: {yval(b['code'])}"]
|
||
if b["type"] == "subcommittee":
|
||
pc = parent_code(b["code"], comm_codes)
|
||
lines.append(f"parent: {yval(body_id(pc, bodies_by_code, comm_codes))}")
|
||
lead = leaders_of.get(b["code"], [])
|
||
if lead:
|
||
lines.append("leadership:")
|
||
for person_name, role, pid in lead:
|
||
lines.append(f" - person: {yval(person_name)}")
|
||
lines.append(f" role: {yval(role)}")
|
||
lines.append("sources:")
|
||
lines.append(" - field: definition")
|
||
lines.append(" source: congress-legislators (unitedstates project)")
|
||
lines.append("confidence: official")
|
||
tags = ["body", b["type"], b["chamber"]]
|
||
lines.append("tags: [" + ", ".join(dict.fromkeys(tags)) + "]")
|
||
lines.append(f"timestamp: {yval(CONGRESS_DATE)}")
|
||
|
||
body = [f"# {b['name']}", "",
|
||
f"{TYPE_LABEL.get(b['type'], 'Body')} ({b['chamber']}).", ""]
|
||
if lead:
|
||
body += ["## Leadership", ""]
|
||
for person_name, role, pid in lead:
|
||
body.append(f"- {role}: {person_name}")
|
||
body.append("")
|
||
if members_count:
|
||
body += [f"{members_count} members "
|
||
"(see each member's file for their seat on this body).", ""]
|
||
body += ["## Source", "",
|
||
"- definition: congress-legislators (unitedstates project)"]
|
||
return "---\n" + "\n".join(lines) + "\n---\n\n" + "\n".join(body) + "\n"
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Candidate files (FEC)
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
STANCE = {"I": "incumbent", "C": "challenger", "O": "open-seat"}
|
||
CAND_STATUS = {"C": "statutory candidate", "N": "not yet a candidate",
|
||
"P": "prior candidate", "F": "future candidate"}
|
||
PARTY_FULL = {"REP": "Republican", "DEM": "Democratic", "IND": "Independent",
|
||
"LIB": "Libertarian", "GRE": "Green",
|
||
"DFL": "Democratic-Farmer-Labor"}
|
||
|
||
|
||
def candidate_name(raw):
|
||
"""'CARL, JERRY LEE, JR' -> 'Jerry Lee Carl Jr'."""
|
||
parts = [p.strip() for p in raw.split(",")]
|
||
last = parts[0] if parts else raw
|
||
first = parts[1] if len(parts) > 1 else ""
|
||
suffix = parts[2] if len(parts) > 2 else ""
|
||
return " ".join(x for x in (first, last, suffix) if x).title()
|
||
|
||
|
||
def candidate_office(rec):
|
||
return "U.S. Senate" if rec.get("office") == "S" else "U.S. House"
|
||
|
||
|
||
def candidate_seat(rec):
|
||
st = rec.get("state") or ""
|
||
if rec.get("office") == "H" and rec.get("district"):
|
||
return f"{st}-{rec['district']}"
|
||
return st
|
||
|
||
|
||
def candidate_frontmatter(rec, disp):
|
||
office, seat, yr = candidate_office(rec), candidate_seat(rec), rec.get("election_year")
|
||
lines = ["type: Candidate", f"title: {yval(disp)}",
|
||
f"description: {yval(f'{office} candidate, {seat} ({yr})')}",
|
||
f"office: {yval(office)}"]
|
||
if rec.get("state"):
|
||
lines.append(f"state: {yval(rec['state'])}")
|
||
if rec.get("office") == "H" and rec.get("district"):
|
||
lines.append(f"district: {yval(seat)}")
|
||
if rec.get("party"):
|
||
lines.append(f"party: {yval(rec['party'])}")
|
||
if rec.get("incumbent_challenge") in STANCE:
|
||
lines.append(f"stance: {yval(STANCE[rec['incumbent_challenge']])}")
|
||
if yr:
|
||
lines.append(f"election_year: {yr}")
|
||
if rec.get("candidate_status") in CAND_STATUS:
|
||
lines.append(f"status: {yval(CAND_STATUS[rec['candidate_status']])}")
|
||
if rec.get("committee_name"):
|
||
lines.append("committee:")
|
||
lines.append(f" name: {yval(rec['committee_name'])}")
|
||
if rec.get("committee_id"):
|
||
lines.append(f" id: {yval(rec['committee_id'])}")
|
||
lines.append("ids:")
|
||
lines.append(f" fec: {yval(rec['fec_id'])}")
|
||
lines.append("sources:")
|
||
lines.append(" - field: filing")
|
||
lines.append(" source: FEC (Federal Election Commission)")
|
||
lines.append("confidence: official")
|
||
tags = ["candidate", "federal",
|
||
"senate" if rec.get("office") == "S" else "house"]
|
||
if rec.get("state"):
|
||
tags.append(rec["state"].lower())
|
||
lines.append("tags: [" + ", ".join(tags) + "]")
|
||
lines.append(f"timestamp: {yval(CONGRESS_DATE)}")
|
||
return lines
|
||
|
||
|
||
def candidate_body(rec, disp):
|
||
office, seat = candidate_office(rec), candidate_seat(rec)
|
||
stance = STANCE.get(rec.get("incumbent_challenge"), "candidate")
|
||
party = PARTY_FULL.get(rec.get("party"), rec.get("party") or "")
|
||
out = [f"# {disp}", "",
|
||
f"{party} {stance} for {office} ({seat}), {rec.get('election_year')}.", ""]
|
||
if rec.get("committee_name"):
|
||
cid = f" ({rec['committee_id']})" if rec.get("committee_id") else ""
|
||
out += ["## Campaign Committee", "", f"- {rec['committee_name']}{cid}", ""]
|
||
out += ["## Source", "", "- filing: FEC (Federal Election Commission)", "",
|
||
"Federal candidate filing; not yet linked to an officeholder record."]
|
||
return out
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Jurisdiction demographic nodes (ACS)
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
def numify(v):
|
||
if v is None or v == "":
|
||
return None
|
||
if isinstance(v, (int, float)):
|
||
return v
|
||
try:
|
||
f = float(v)
|
||
return int(f) if "." not in str(v) and f == int(f) else round(f, 4)
|
||
except (ValueError, TypeError):
|
||
return v
|
||
|
||
|
||
def canonical_fips(fips):
|
||
return re.sub(r"\D", "", str(fips))[-5:]
|
||
|
||
|
||
DEMOG_LABELS = {
|
||
"population": "Population", "population_under_18": "Under 18",
|
||
"population_18_64": "18–64", "population_65_plus": "65+",
|
||
"median_household_income": "Median household income",
|
||
"poverty_rate": "Poverty rate", "homeownership_rate": "Homeownership rate",
|
||
"unemployment_rate": "Unemployment rate", "median_home_value": "Median home value",
|
||
"gini_index": "Gini index", "vacancy_rate": "Vacancy rate",
|
||
"race_white": "White", "race_black": "Black", "race_asian": "Asian",
|
||
"race_native": "Native", "hispanic": "Hispanic/Latino",
|
||
"bachelors_plus": "Bachelor's or higher",
|
||
}
|
||
|
||
|
||
def normalize_demog(row):
|
||
def pick(*cols):
|
||
for c in cols:
|
||
if row.get(c) not in (None, ""):
|
||
return numify(row.get(c))
|
||
return None
|
||
d = {
|
||
"population": pick("total_population"),
|
||
"population_under_18": pick("population_under_18"),
|
||
"population_18_64": pick("population_18_64"),
|
||
"population_65_plus": pick("population_65_plus"),
|
||
"median_household_income": pick("median_household_income"),
|
||
"poverty_rate": pick("poverty_rate"),
|
||
"homeownership_rate": pick("homeownership_rate"),
|
||
"unemployment_rate": pick("unemployment_rate"),
|
||
"median_home_value": pick("median_home_value"),
|
||
"gini_index": pick("gini_index"),
|
||
"vacancy_rate": pick("vacancy_rate"),
|
||
"race_white": pick("race_white"), "race_black": pick("race_black"),
|
||
"race_asian": pick("race_asian"), "race_native": pick("race_native"),
|
||
"hispanic": pick("hispanic_latino", "hispanic"),
|
||
}
|
||
bp = pick("edu_bachelors_plus")
|
||
if bp is None:
|
||
bp = (pick("edu_bachelors") or 0) + (pick("edu_graduate") or 0) or None
|
||
d["bachelors_plus"] = bp
|
||
return {k: v for k, v in d.items() if v is not None}
|
||
|
||
|
||
def demog_yaml(demog):
|
||
lines = ["demographics:"]
|
||
for k, v in demog.items():
|
||
lines.append(f" {k}: {yval(v) if isinstance(v, str) else v}")
|
||
return lines
|
||
|
||
|
||
def demog_table(demog):
|
||
out = ["## Demographics (ACS 2023)", "", "| Measure | Value |", "| --- | --- |"]
|
||
for k, v in demog.items():
|
||
out.append(f"| {DEMOG_LABELS.get(k, k)} | {v} |")
|
||
return out + [""]
|
||
|
||
|
||
def jurisdiction_file(title, classification, extra_fm, demog, st, body_intro):
|
||
lines = ["type: Jurisdiction", f"title: {yval(title)}",
|
||
f"classification: {classification}"]
|
||
lines += extra_fm
|
||
if demog:
|
||
lines += demog_yaml(demog)
|
||
lines += ["sources:", " - field: demographics",
|
||
" source: Census ACS 2023", "confidence: official",
|
||
f"tags: [jurisdiction, {classification}, {st}]",
|
||
f"timestamp: {yval(CONGRESS_DATE)}"]
|
||
body = [f"# {title}", "", body_intro, ""]
|
||
if demog:
|
||
body += demog_table(demog)
|
||
body += ["## Source", "", "- demographics: Census ACS 2023"]
|
||
return "---\n" + "\n".join(lines) + "\n---\n\n" + "\n".join(body) + "\n"
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# main
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
def load(name):
|
||
return [json.loads(l) for l in (DATA / name).open()]
|
||
|
||
|
||
def main():
|
||
officeholders = load("officeholders-v2.jsonl")
|
||
bodies = load("bodies.jsonl")
|
||
leadership = load("leadership.jsonl")
|
||
memberships = load("committee_memberships.jsonl")
|
||
candidates = load("fec_candidates.jsonl")
|
||
acs_county = load("acs_county.jsonl")
|
||
acs_cd = load("acs_cd.jsonl")
|
||
|
||
bodies_by_code = {b["code"]: b for b in bodies}
|
||
comm_codes = committee_codes(bodies)
|
||
enrichment = build_enrichment(leadership, memberships, bodies_by_code, comm_codes)
|
||
|
||
# reverse index: body code -> its leaders, and -> member count
|
||
leaders_of, members_count = {}, {}
|
||
name_to_pid = {norm_name(r.get("full_name") or ""): r.get("person_id")
|
||
for r in officeholders if r.get("full_name")}
|
||
for m in memberships:
|
||
members_count[m["body_code"]] = members_count.get(m["body_code"], 0) + 1
|
||
role = ROLE_MAP.get(m["committee_title"], "member")
|
||
if role in LEADER_ROLES:
|
||
pid = name_to_pid.get(norm_name(m["person_name"]))
|
||
leaders_of.setdefault(m["body_code"], []).append(
|
||
(m["person_name"], role, pid))
|
||
for v in leaders_of.values():
|
||
v.sort(key=lambda t: (t[1], t[0]))
|
||
|
||
matched = sum(1 for r in officeholders if r["level"] == "federal"
|
||
and norm_name(r.get("full_name") or "") in enrichment)
|
||
|
||
# ---- plan person paths (disambiguate deterministically) ----
|
||
planned = {}
|
||
for rec in sorted(officeholders, key=lambda r: r["person_id"]):
|
||
planned.setdefault((person_dir(rec), slugify(display_name(rec))), []).append(rec)
|
||
person_files = {}
|
||
for (d, base), group in planned.items():
|
||
by_person = {}
|
||
for rec in group:
|
||
slug = base if len(group) == 1 else f"{base}-{rec['person_id'][:8]}"
|
||
by_person.setdefault(slug, []).append(rec)
|
||
for slug, sub in by_person.items():
|
||
for rec in sub:
|
||
final = slug if len(sub) == 1 else f"{slug}-{rec['office_id'][:8]}"
|
||
p = d / f"{final}.md"
|
||
assert p not in person_files, f"collision {p}"
|
||
person_files[p] = rec
|
||
|
||
# ---- write ----
|
||
if OUT.exists():
|
||
shutil.rmtree(OUT)
|
||
|
||
for path, rec in sorted(person_files.items()):
|
||
enr = enrichment.get(norm_name(rec.get("full_name") or "")) \
|
||
if rec["level"] == "federal" else None
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text("---\n" + "\n".join(person_frontmatter(rec, enr))
|
||
+ "\n---\n\n" + "\n".join(person_body(rec, enr)) + "\n",
|
||
encoding="utf-8")
|
||
|
||
for b in sorted(bodies, key=lambda x: x["code"]):
|
||
bid = body_id(b["code"], bodies_by_code, comm_codes)
|
||
path = OUT / (bid + ".md")
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text(body_file(b, bid, bodies_by_code, comm_codes,
|
||
leaders_of, members_count.get(b["code"], 0)),
|
||
encoding="utf-8")
|
||
|
||
# ---- candidates (FEC) ----
|
||
cand_plan = {}
|
||
for rec in sorted(candidates, key=lambda r: r["fec_id"]):
|
||
d = OUT / "us" / "states" / (rec.get("state") or "xx").lower() / "candidates"
|
||
cand_plan.setdefault((d, slugify(candidate_name(rec["candidate_name"]))), []).append(rec)
|
||
n_cand = 0
|
||
for (d, base), group in sorted(cand_plan.items(), key=lambda kv: str(kv[0][0]) + kv[0][1]):
|
||
for rec in group:
|
||
slug = base if len(group) == 1 else f"{base}-{rec['fec_id'][-4:].lower()}"
|
||
disp = candidate_name(rec["candidate_name"])
|
||
path = d / f"{slug}.md"
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text("---\n" + "\n".join(candidate_frontmatter(rec, disp))
|
||
+ "\n---\n\n" + "\n".join(candidate_body(rec, disp)) + "\n",
|
||
encoding="utf-8")
|
||
n_cand += 1
|
||
|
||
# ---- county jurisdictions (ACS, deduped by canonical fips) ----
|
||
oh_count = {}
|
||
for rec in officeholders:
|
||
if rec["level"] in ("county", "municipal"):
|
||
key = ((rec.get("state_abbr") or "").lower(), county_slug(rec))
|
||
oh_count[key] = oh_count.get(key, 0) + 1
|
||
seen_fips, n_county = set(), 0
|
||
for row in sorted(acs_county, key=lambda r: canonical_fips(r["county_fips"])):
|
||
cf = canonical_fips(row["county_fips"])
|
||
if cf in seen_fips:
|
||
continue
|
||
seen_fips.add(cf)
|
||
st = row.get("state_abbr")
|
||
if not st:
|
||
continue
|
||
base = re.sub(r",\s*[A-Z]{2}$", "", row.get("county_name") or "")
|
||
cslug = slugify(norm_county(re.sub(r"\s+County$", "", base)))
|
||
title = f"{base}, {st}"
|
||
demog = normalize_demog(row)
|
||
ohc = oh_count.get((st.lower(), cslug), 0)
|
||
intro = (f"County jurisdiction — {ohc} officeholders mapped."
|
||
if ohc else "County jurisdiction.")
|
||
extra = [f"fips: {yval(cf)}", f"state: {yval(st)}"]
|
||
path = OUT / "us" / "states" / st.lower() / "counties" / cslug / "index.md"
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text(jurisdiction_file(title, "county", extra, demog,
|
||
st.lower(), intro), encoding="utf-8")
|
||
n_county += 1
|
||
|
||
# ---- congressional-district jurisdictions (ACS) ----
|
||
n_cd = 0
|
||
for row in sorted(acs_cd, key=lambda r: (r["state_abbr"], r["district"])):
|
||
st, dist = row["state_abbr"], row["district"]
|
||
title = f"{st}-{dist}"
|
||
demog = normalize_demog(row)
|
||
extra = [f"state: {yval(st)}", f"district: {yval(title)}"]
|
||
path = OUT / "us" / "states" / st.lower() / "districts" / f"{dist}.md"
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text(jurisdiction_file(title, "congressional-district", extra,
|
||
demog, st.lower(),
|
||
f"Congressional district {title}."),
|
||
encoding="utf-8")
|
||
n_cd += 1
|
||
|
||
print(f"candidate files: {n_cand} county jurisdictions: {n_county} "
|
||
f"district jurisdictions: {n_cd}")
|
||
print(f"person files: {len(person_files)} "
|
||
f"(federal enriched: {matched}/{sum(1 for r in officeholders if r['level']=='federal')})")
|
||
print(f"body files: {len(bodies)} "
|
||
f"(chambers/exec {sum(1 for b in bodies if b['type'] in INSTITUTIONS)}, "
|
||
f"committees {sum(1 for b in bodies if b['type']=='committee')}, "
|
||
f"subcommittees {sum(1 for b in bodies if b['type']=='subcommittee')})")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|