Dillon Ring

Student & Developer

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:

E=∑d(gd+2 [gd>0])+λcut (cut edges)+λcounty (county fragments)E = \sum_{d} \left( g_d + 2\,[g_d > 0] \right) + \lambda_{\text{cut}}\,(\text{cut edges}) + \lambda_{\text{county}}\,(\text{county fragments})

Here, gdg_d is the number of percentage points district dd 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 e−ΔE/Te^{-\Delta E / T}, and TT 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 (4πA/P24\pi A / P^2), 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.

StackReact, MapLibre, PMTiles, Rust (WASM), Hono, Cloudflare Workers, R2, D1
StatusFree
LinkOpen project