districtry
Click the map for every district you're in, and who represents you.
Pick a point on the map. The app looks it up against every civic-district boundary you've toggled on — ward, county board, congressional, police, school, and dozens more — and builds a "civic profile" for that exact spot, naming the district and, wherever a verifiable source exists, the person who represents it. No login, no build step, no server: it's a single HTML file with an inline script, deployed as a static site.
The fleet
This repo publishes seventeen instances of the same app, one per place, each its own self-contained folder:
| Metro | Live at | Covers |
|---|
| Illinois (reference implementation) | districtry.com/il/ | 97 counties — wards, county boards, police districts and beats, school zones, judicial subcircuits, and the people who hold those seats |
| New York | districtry.com/ny/ | Counties, towns and villages, school and judicial districts across the state, plus the city's boroughs, Council and community districts, NYPD precincts and school zones |
| San Francisco | districtry.com/ca/ | Supervisor districts, neighborhoods, police districts, school attendance areas, and the state and federal seats above them |
| Wisconsin | districtry.com/wi/ | 72 counties — cities, villages and towns, school districts, ZIP codes, and the Assembly, Senate and U.S. House seats, with who holds them |
| Iowa | districtry.com/ia/ | 99 counties — supervisor districts under each county's own election plan, townships and cities, school districts, ZIP codes and post offices, and the Iowa Senate, House and U.S. House seats, with who holds them |
| Michigan | districtry.com/mi/ | 83 counties — commissioner districts, compiled by the state from the plan each county's apportionment commission files, and the Michigan Senate, House and U.S. House seats, with who holds them |
| Minnesota | districtry.com/mn/ | 87 counties — townships and cities, school districts, ZIP codes, and the nearest police station, fire station and post office, plus the Minnesota Senate, House and U.S. House districts, with who holds the U.S. House seats |
| Kentucky | districtry.com/ky/ | 120 counties — the county a point sits in, and the Kentucky Senate, House and U.S. House districts, with who holds the U.S. House seats |
| Indiana | districtry.com/in/ | 92 counties — townships, cities and towns, school districts, ZIP codes, and the nearest police station, fire station and post office, plus the Indiana Senate, House and U.S. House districts, with who holds the U.S. House seats |
| North Carolina | districtry.com/nc/ | 100 counties — townships, cities and towns, school districts, ZIP codes, and the nearest police station, fire station and post office, plus the North Carolina Senate, House and U.S. House districts, with who holds every one of those seats |
| Oklahoma | districtry.com/ok/ | 77 counties — cities and towns, school districts, ZIP codes, and the nearest police station, fire station and post office, plus the Oklahoma Senate, House and U.S. House districts, with who holds the U.S. House seats and nearly every legislative one |
| South Carolina | districtry.com/sc/ | 46 counties — county council districts with the winner of every seat, cities and towns, school districts, ZIP codes, the Catawba Nation's land, and the nearest police station, fire station and post office, plus the South Carolina Senate, House and U.S. House districts, with who holds every one of those seats |
| Tennessee | districtry.com/tn/ | 95 counties — county commission districts, cities and towns, school districts, ZIP codes, and the nearest police station, fire station and post office, plus the Tennessee Senate, House and U.S. House districts, with who holds each of those seats and the county commissioners in every county that publishes a current list |
| Arkansas | districtry.com/ar/ | 75 counties — quorum court districts with each county's judge and the justice of the peace for nearly every district, cities and towns, school districts, ZIP codes, and the nearest police station, fire station and post office, plus the Arkansas Senate, House and U.S. House districts, with who holds each of those seats |
| Mississippi | districtry.com/ms/ | 82 counties — every county's five supervisors named, with the supervisor districts drawn in 25 counties, cities and towns, school districts, ZIP codes, the Mississippi Band of Choctaw Indians' land, the Supreme Court, Court of Appeals, chancery and circuit court districts with their judges, and the nearest police station, fire station and post office, plus the Mississippi Senate, House and U.S. House districts, with who holds each of those seats |
| Louisiana | districtry.com/la/ | 64 parishes — every parish's police jury or council named, with its president where it elects one, and the parish district lines drawn in the 8 parishes whose current map is confirmed, cities, towns and villages with their mayors and councils and the police chief where one is elected, the council districts of Lafayette, Bossier City, Central and Slidell, school districts with every school board's members, ZIP codes, the land of Louisiana's four federally recognized tribes, and the nearest police station, fire station and post office, plus the Louisiana Senate, House and U.S. House districts, with who holds each of those seats |
| Missouri | districtry.com/mo/ | 114 counties and the City of St. Louis — every county's commission, council or legislature named (the three charter counties that keep their own lists from their own pages), with the commission district lines drawn in the 35 counties whose own maps give them, cities, towns and villages, townships, school districts, ZIP codes, and the nearest police station, fire station and post office, plus the Missouri Senate, House and U.S. House districts, with who holds each of those seats |
They share one engine — the metro-agnostic core, layer-registration framework, and UI chrome live once under engine/ and are spliced into each instance's own index.html/sw.js by scripts/compose_app.py, so there's nothing to keep in sync by hand. What's genuinely per-metro (which layers exist, their boundary sources, the officeholder rosters) lives in that instance's own metro-worksheet.json, data/, and layer modules. Illinois is by far the most built out — the rest of this README is mostly about it — and is the model the others follow when a feature or a county-expansion technique proves out. Wisconsin was the first state to expand IN PLACE as a folder rather than as a fork; Missouri is the newest.
What it answers
41 layers ship in Illinois today, split political (12) · public safety (7) · schools (9) · geography (13). Layers are location-aware: Chicago-only layers hide once you're outside the city, county-scoped layers only appear in counties that publish the underlying data, and the statewide layers (county, township, municipality, school district, ZIP) work anywhere in Illinois.
| Group | Layer | What you get |
|---|
| Political | City Ward | Ward number, alderman, office phone + address |
| Ward Precinct | Precinct number (a sub-selection of City Ward — turning it on drops the ward to an outline and fills it with its precincts) |
| County Board District | Your county-legislature seat, dispatched across 61 counties — Cook's Commissioner district (live officeholder join, office pin) through the collar counties to the downstate additions. Depth follows what each county publishes: members with party, phone, e-mail and profile links where a roster exists (most), name-only where it doesn't. Where a county publishes no boundary at all, the district is derived from its own published composition (whole townships or whole precincts) and the build proves the partition and population balance. Nineteen counties elect their board countywide instead — no district to draw, so their commissioners ride the County card instead, each verified at-large from a certified election document or the county's own election authority in writing |
| U.S. House District | District (IL-N), representative, party, D.C. phone, website |
| IL State Senate District | Senator, party, Springfield + district offices, ILGA page |
| IL State House District | State representative, party, offices, ILGA page |
| Elected School Board District | ERSB district ("6b"), elected board member |
| IL Supreme Court District | District under PA 102-0011 (District 1 = Cook County) |
| Cook County Board of Review District | District under PA 102-0012 (property-tax appeals) |
| Early Voting Site (nearest 3) | Official early-voting sites for the current cycle — site, ward, address, distance (hand-curated per election from chicagoelections.gov; each site also hosts a secured ballot drop box) |
| Judicial Subcircuit | Your judicial subcircuit — dispatched from 9 shipped circuits whose boundaries can span more than one county: Cook (20 subcircuits, with the Circuit Court's Municipal District + courthouse), Will (12th Cir.), DuPage (18th), Lake (19th), Kane (16th), McHenry (22nd), Winnebago (17th, shared with Boone), Madison (3rd, shared with Bond), Sangamon (7th, shared with Greene, Jersey, Macoupin, Morgan & Scott). Kane and McHenry pre-built from the enacted PA 102-0693 shapefile; each card links its circuit's court. (Kendall's 23rd Circuit has no subcircuits under the act, so the layer hides there) |
Every result card fails independently: a layer whose data source is down shows an error with a Retry button in that card and never affects the others. And where the app has nothing to show, it says so instead of guessing — officeholder data is never invented, and the in-app Data gaps panel lists every recorded absence (105 as of 25 August 2026) naming the specific artifact its publisher would have to release.
Shareable links
The URL hash mirrors your current view (#p=41.8825,-87.6285&l=wd,sb — each layer is written as a short code that never changes, listed in layer-codes.json). Copy it from the URL bar — or use the Copy link button on the selected-point chip — and anyone opening the link sees the same point with the same layers on. Links made before 2026-10-09 spell the view out (#point=41.88250,-87.62850&layers=ward,school-board) and still open.
How Illinois coverage grew
Illinois started at the seven Chicago-metro counties and now covers 89 of Illinois's 102 counties — 77 through their own dispatch entries, 2 through a shipped judicial circuit's boundary alone, and 10 at-large through the County card — reaching north to the Wisconsin line, west to the Mississippi, and south through the Metro East to the Missouri border. The generated county-status table is the current source of truth; the remaining 13 researched-but-unserved counties each carry a recorded gap explaining what's missing.
Growth stopped following simple map adjacency partway through and now goes wherever the data actually is. Some counties publish a GIS site with everything needed; several were built with no county GIS at all, either dissolved from Census voting districts against a certified election canvass, or — for a handful of counties whose own board pages were never readable — reconstructed entirely from certified election returns published by a third-party results vendor. Two counties (Douglas, Vermilion) shipped from a public ArcGIS Online organization that carried dozens of the county's own layers even though the county's own web map viewer only ever pointed at one of them.
The coverage area is a single contiguous polygon today — no islands, though the boundary has gone disjoint and reattached four separate times as adjacency-driven growth outran itself (most recently Massac County, detached and reattached to the mainland within one evening). It does carry four holes: unserved ground entirely enclosed by served counties — Bureau and Christian standing alone, plus two multi-county blocks (Clay/Fayette/Jasper/Marion/Wayne, and Champaign/Ford/Piatt) sealed shut when a single bordering county's join closed the last gap in the ring. scripts/build_metro_outline.py --check is the ground truth for the ring count (5: one outer boundary, four holes) — a hole isn't permanent, and has closed before when a county inside it joined.
Try it locally
There's nothing to build.
# any static server works:
python3 -m http.server 8000
# then open http://localhost:8000/ for the fleet landing page, or
# http://localhost:8000/il/ to go straight to the Illinois app
Cook and the collar counties mostly fetch live data from public APIs at runtime, so they need an internet connection. Most downstate counties publish no live API at all, so their boundaries are pre-built once and shipped as same-origin files under il/data/app/ — 271 files today: 194 boundary/geometry files served cache-first by the service worker (il/sw.js), since boundaries change roughly once a decade, so a loaded layer keeps working offline; and 77 officeholder-roster files served network-first, so a returning visitor always gets the current roster rather than a stale one.
How it's built
Each instance is a stable core plus pluggable layer modules, all inside one index.html. The full module contract and build history live in docs/BUILD_PLAYBOOK_1.md; docs/EXPANSION_GUIDE.md — the State Expansion Guide — is the primary guide for standing up a new state instance, deepening one county by county, or adding a new concept.
- Core: Leaflet map, click-to-select + Photon/Nominatim geocoder (debounced, metro-bounded), global
{selectedPoint, sequence} state where a monotonic sequence counter discards stale async results once a newer point is selected, shared sanitize / pointInGeometry / fetchJSONWithRetry utilities, the layer registry + result-card framework with per-layer failure isolation, selected-boundary highlight, URL-hash permalinks.
- Layer modules: each layer registers
{id, group, label, overlay:{load, style}, query(point, seq), render(result)}. Overlays lazy-load on first toggle and are cached; query runs a local point-in-polygon test against the cached boundaries (or nearest-N haversine for station/school/amenity layers). A layer can declare a coverage(point) test — outside it, the layer hides rather than erroring. The six cross-county concepts (County Board, Judicial Subcircuit, Fire Protection District, Park District, Library District, Voting Precinct) each register through one registerCountyLayer dispatcher — a single toggle holding a per-county entry table, whose coverage is the OR of its counties' — so adding a county to a shipped concept is a dispatch-table entry, not a new layer.
- Result cards: lead with the layer name, then the district identifier, then — wherever a verifiable source exists — the officeholder(s), office location, contact info, and a link to more detail, in that order. A layer with no representative (a ZIP, a community area) just omits those rows.
- Honesty rules, enforced in review: officeholder data is never guessed — where no verifiable roster source exists, a card links to the official body instead of inventing a name. Every external string is sanitized or rendered via
textContent.
- One engine, seventeen instances: the metro-agnostic parts — core, registry, UI chrome, sub-page shell — live once under
engine/, fenced with ENGINE:BEGIN/END markers, and scripts/compose_app.py splices them into il/, ny/, ca/, wi/, ia/, mi/, mn/, ky/, in/, nc/, ok/, sc/, tn/, ar/, ms/, la/ and mo/'s own index.html/sw.js/sub-pages. --check recomposes in memory and fails on any drift, so there's no separate release channel to keep in step — the composed, committed files are exactly what gets served.
Data sources
The reader-facing version of this is districtry.com/il/sources.html: the same credits below, plus a layer matrix giving every registered layer its own row — what it answers, the publisher its boundary comes from, where the names on its card come from, and the ground it answers on. It's generated from metro-worksheet.json's layers[].source, off the same list that drives the layer registry, so it can't fall behind the app.
The table below covers the Chicago-metro sources; the several dozen downstate county GIS hosts, election-results platforms, and county-run scrapers behind the rest of the fleet are cataloged per-county in docs/COUNTY_STATUS.md and per-layer on the sources page. Three commercial election-results platforms carry certified canvasses for dozens of Illinois counties each and back a meaningful share of the fleet's downstate boards and precincts where no county GIS exists: il-<county>.pollresults.net / .accessliberty.com, platinumelectionresults.com, and results.gbsvote.com / results.enr.clarityelections.com. In every case the county's own certified canvass is the cited source — the platform is only the delivery mechanism.
| Source | Used for |
|---|
| Chicago Data Portal (Socrata) | Wards + aldermen roster, ward precincts, library locations, CPS zones + networks, community areas |
CPD ArcGIS (services2.arcgis.com/t3tlzCPfmaQzSWAk) | Police district boundaries, police beat boundaries, police station roster, school locations |
| chicagopolice.org per-district pages (scraped weekly by CI) | Police district commander, CAPS unit phone/email, station address |
| ccpsa.chicago.gov per-council pages (scraped weekly by CI) | CCPSA District Council elected Councilors — name + role per police district; boundaries reuse the CPD police-district geometry |
Cook County GIS (gis.cookcountyil.gov/traditional/rest/services) | Cook Commissioner districts + live officeholder table; the Clerk's library, fire-protection, and park tax-agency tilings; the Clerk's current suburban voting precincts; the Clerk's TIF tiling and the MWRD boundary |
| U.S. Census TIGERweb | Live statewide layers (County, Township, Municipality, the three School District layers, ZIP/ZCTA), plus the pre-built U.S. House / IL Senate / IL House boundaries |
| Will · DuPage · Lake · Kane · McHenry · Kendall County ArcGIS (collar-county GIS hosts) | Judicial subcircuits, board districts, fire/park/library districts, voting precincts, varying in depth by what each county's GIS carries |
| willcountyillinois.gov, kanecountyil.gov, dupagecounty.gov (scraped weekly by CI); kendallcountyil.gov, mchenrycountyil.gov (also scraped weekly by CI, since 2026-09-03 — these two need a stdlib HTTP client sending browser client hints, which their Akamai edge accepts where is refused) |
The boundary layers in il/data/app/ are topology-preserving simplifications (mapshaper) of the official shapefiles; full-precision GeoJSON conversions live in il/data/, untouched originals in il/data/source/raw/. The simplified copies agreed with full precision on 100% of 2,000 random in-city test points.
Repository layout
The fleet lives in one repo. Each metro is a self-contained instance folder — its own app, service worker, data, sub-pages, README.md, CLAUDE.md, docs/ and scripts/ — and the repo root holds only what's genuinely fleet-wide.
index.html · sw.js the fleet LANDING PAGE (generated from metros.json) and its
service-worker kill switch — not the app; see il/ below
county-board.html, police-district.html,
school-board.html thin redirect stubs for old chidistricts.com URLs, forwarding
to their il/ equivalent (search-equity holdovers, not live pages)
privacy.html one privacy page for every instance (generated by MEASURING each
app's shipped index.html, not from a hand-kept manifest)
metros.json the fleet manifest — one entry per metro (id, url, landing blurb)
engine/ the one shared copy of the metro-agnostic engine, spliced into
every instance by scripts/compose_app.py
scripts/ fleet-wide builders/gates: compose_app.py, generate_metro_files.py,
build_landing_page.py, build_privacy_page.py, build_county_status.py,
fleet_status.py, and the IL data-pipeline scripts (see il/scripts/)
docs/ the expansion guide, fleet layer guidebook, dev-process log, archives
schema/metro-worksheet.schema.json validates every instance's metro-worksheet.json before it's used
districtry/ brand source: design canvases, design tokens, icons
fonts/ the landing page's own self-hosted font (distinct from il/fonts/)
WATCH.md the redistricting watch calendar
CNAME · sitemap.xml · robots.txt districtry.com domain + SEO plumbing
il/ ILLINOIS — the reference implementation this README mostly covers
index.html the entire app: styles, core, all layer modules — composed from
engine/ plus this instance's own config and layer modules
sw.js service worker (cache-first geometry, network-first rosters)
metro-worksheet.json this instance's per-fork facts; regenerates its own GENERATED regions
sources.html, faq.html, county-board.html,
police-district.html, school-board.html sub-pages, composed the same way as index.html
history.html this deployment's own record — generated by
scripts/build_history_page.py: stat tiles measured from
the shipped data files, changelog dated and append-only
data/app/ 271 files the page fetches — boundary geometry (cache-first) and
officeholder rosters (network-first)
data/ · data/source/raw/ full-precision conversions and untouched originals
(scripts/ — at the REPO ROOT) Illinois runs from the root, not from il/: one scraper + builder
pair per roster (ilga_scraper.py, build_congress_roster.py,
cpd_district_scraper.py, ~80 county-board/precinct/fire/park/library
builders...), plus Illinois's own validate_index.py and
smoke_test.mjs, beside the fleet tooling. There is no il/scripts/.
(docs/ — at the REPO ROOT) COUNTY_STATUS.md (generated per-county table), DATA_LAYER_GUIDEBOOK.md
(fleet layer inventory), EXPANSION_GUIDE.md, BUILD_PLAYBOOK_1.md
ny/ NEW YORK CITY — same shape as il/
ca/ SAN FRANCISCO — same shape as il/
.github/workflows/ weekly roster refreshes (each opens a PR for human review — never
auto-committed), the per-PR smoke test, monthly source-freshness
checks, weekly fleet-status, Pages deploy
Validation
smoke-test.yml runs on every pull request against every instance, the ones not yet published included. Roughly in order:
- Composition and generation drift gates (stdlib-only Python, run before anything is installed):
generate_metro_files.py --check (every GENERATED:BEGIN/END region matches what metro-worksheet.json renders), compose_app.py --check (every instance carries the shared engine/ blocks byte-for-byte, nothing hand-edited in place), plus generators for the brand tokens, the coverage-gaps panel, the county-status table, the dark-mode map palette, the landing page, the privacy page (which measures each shipped app rather than trusting a manifest), the per-instance history page (whose stat tiles are measured from the shipped data files, so a roster refresh that moves a number must regenerate it), and the installable web-app manifest.
- Static merge gate (
scripts/validate_index.py, run once per instance): the inline script passes node --check, every declared layer id is still registered (41 for Illinois), no dataset is embedded inline, every data/app/*.json file is present and shape-checked, and the sources page covers every registered layer.
- Other stdlib gates: a lint against a
# comment silently truncating a backslash-continued shell command, a check that every workflow script imports only under what that workflow actually installs, a check that a roster whose scraper is blocked still carries an honest seat count, and a roster-retention gate that compares every roster field against the same file at the PR's base and fails when a field — a whole county's e-mail column going silently empty, say — stops being published.
- Behaviour gate (
scripts/smoke_test.mjs, once per instance, served together from one local static server): a real Chromium boot via Playwright asserts each app comes up, registers all its layers, classifies a known point against known ground truth (Illinois: 41.88250,-87.62850 in the Loop resolves school board district 6b, IL Supreme Court district 1, Board of Review district 3, including the school-board member-roster join), and degrades to an isolated error card + Retry when a data source fails. Alongside it: a check that the root landing page and its old-URL forwarding both work in a real browser, and a check that every page in sitemap.xml carries the same brand and standing links.
- Monthly / weekly, not per-PR: a source-freshness check (upstream dataset ids still resolve; a newer-year Socrata edition gets flagged) and a link-gate check (every URL a card or roster actually renders still resolves) fold into one tracking issue on any WARN/FAIL rather than editing anything; a weekly fleet-status run diffs every instance's layer roster and gaps file against
docs/DATA_LAYER_GUIDEBOOK.md.
Expanding it
docs/EXPANSION_GUIDE.md — the State Expansion Guide — is the primary guide for standing up a new state instance, deepening one county by county and city by city, or adding a new concept; its Part 5 collects what Illinois's 93 counties and Wisconsin's four phases taught. Start there. docs/DATA_LAYER_GUIDEBOOK.md is the fleet-wide layer inventory (what exists where, recorded parity debts, the backlog). Officeholder data is never guessed and a county is never added on a hunch — every roster and every boundary in this repo traces to a named, checkable public source.
Licence
The code and the data are licensed separately, because this project holds
different rights in each.
Code — Apache License 2.0. Everything that produces the apps: the
inline app source, the shared engine/ blocks, every builder, scraper,
generator and gate under scripts/, and the CI workflows. Apache-2.0 rather
than MIT for its explicit patent grant and its requirement that a modified file
say it was changed — both worth having on civic infrastructure someone else may
fork for their own state.
Data — Open Database License (ODbL) v1.0. Everything
under */data/: the shipped data/app/ files each app fetches at runtime, and
the intermediate and source files they are built from.
The split matters, and so does its limit. Almost nothing here is original
observation — the contribution is the compilation: deciding which public
records answer a question, fetching them, reconciling them against each other,
and assembling the result into something queryable. The ODbL grant is over that
compilation. It is not a grant over the underlying records, which are not this
project's to give. Census geometry is public domain; state, county and
municipal records are public records this project asserts no ownership of; and
one shipped file — Jo Daviess County's board districts, purchased under a
licence that permits display and forbids redistribution — is excluded from the
grant entirely. If you are redistributing this data in bulk, read
LICENSE-DATA.md first and drop that file.
Third-party components and their licences are listed in NOTICE.
Not for legal or official use
Boundary and roster data come from public sources that explicitly disclaim legal precision. Always confirm district assignments and officeholders with the relevant government office before relying on them for anything official.