Redistricting.dev is a tool for drawing electoral district maps. It began as a Texas-only project and now covers most states and a few other countries. You pick a region, assign census units to districts, and watch population, demographics, and past election results change as you go.
Editing
You can edit Census block groups, or precincts from 2024, depending on the state. You click, lasso, select, or brush them into districts. Plans can be saved, shared by link, or exported as a CSV.
Painting a whole state by hand is slow, so we also built a query tool. You can select every precinct in a county, on a street, above an income line, within a few miles of the map center, or inside a vote-share range (as well as any combination of these criteria) and then assign the result to a district in one step.
Auto-redistricter
Automatic redistricting is not a new idea, and neither is the method we used. The auto-redistricter uses Recombination, a method from the MGGG Redistricting Lab, and my solver follows the approach of frcw.rs, an existing Rust implementation. ReCom merges two adjacent districts, picks a random spanning tree over the merged area, and cuts it somewhere that leaves both halves contiguous and within the population tolerance. Then it repeats.
This version is written in Rust and compiled to WebAssembly, so it can run in your browser. It is open source as recom-core.
Map Agent
You tell the map agent what you want, something like "six competitive districts
in Texas," and a language model works out how to get there. It can't move
precincts itself. What it can do is fill in a JSON recipe: where each district
should start, what to prefer as it grows, and what the finished map has to look
like. A Rust solver called region-grow then builds the map from that recipe.
Since the model only produces data, a bad recipe just gets rejected before
anything runs.
For that request, a recipe would look roughly like this:
{
"objectives": [{
"label": "Competitive",
"numerator": "president2024Dem",
"denominator": ["president2024Dem", "president2024Rep"],
"minPct": 47,
"maxPct": 53,
"count": 6
}],
"steps": [
{ "grow": {
"district": 1,
"seed": { "region": "r1" },
"priority": { "near": { "target": 0.5, "priority": {
"ratio": ["president2024Dem",
["president2024Dem", "president2024Rep"]] } } } } },
{ "fillRemaining": {} },
{ "balance": {} },
{ "optimize": { "iterations": 300000 } }
]
}
An actual version the agent would execute has a grow step for every district it seeds, and r1 would be a
group of counties (think, metro area, swing area) the agent defined earlier. Growing a district repeatedly
adds whichever neighboring precinct ranks highest under the priority, so it can ensure it
stays contiguous. Here the priority favors precincts near a 50%
two-party share. fillRemaining and balance then finish the map and even out
population.
Growing gets you a plausible starting point, but not yet a plan that meets the target.
That is the job of optimize, which runs simulated annealing over the finished
map. Each district is scored by how far its ratio sits outside the target band:
Here, is the number of percentage points district falls outside its band.
The flat 2 for any miss, means landing one district inside the band is worth more
than nudging several closer. With count set, only the districts nearest the
band are scored, "six competitive seats" leaves the search free to pick which
six.
Moves are either a single precinct flipping to a neighbor, or, every fiftieth step, a full recombination of two districts. Flips alone can't restructure a plan once the population tolerance is tight, because a district can only drift a few precincts before it needs a compensating move. Recombinations can. A worse plan is accepted with probability , and falls by a factor of a thousand over the run. Contiguity, locks, and population tolerance are never violated.
The agent is also instructed to work like an analyst instead of a generator. It looks at which counties hold the relevant population before it seeds anything, views a rendered image of each candidate to catch tendrils and holes, tries a different strategy, and reruns its favorite with several seeds to see how much the outcome depends on luck. It compares candidates on population deviation, cut edges, county splits, and compactness (), and it is told to call the winner "best found by this search," never optimal, and to say a target was met only if the solver's own report says so.
The same tools edit existing maps. The enacted plan is always available as a
starting point, and moving an area into a district becomes an assign step that
locks it, since otherwise the balancing pass would push it right back. The agent
needs a signed-in account and runs on a model through OpenRouter.
Elections
Swing mode starts from real 2024 results in every district. Sliders adjust turnout and vote choice by race, education, and age, and a Monte Carlo simulation turns that into district ratings and a range of seat counts. The Senate view combines VoteHub polling with precinct-level results from past elections. There is also DistrictGuessr, a daily game where you identify a district from its outline.
Infrastructure
We store a lot of map tiles that we then serve over Cloudflare R2. The main app is a React app running on a Cloudflare Worker. The same Worker handles the Hono API. We do auth through Oauth2 via Passport, my own identity provider.