Inspiration
What it does## Inspiration
We live in Northern California fire country. Our families have evacuated more than once, and every time, the hardest part wasn't packing. It was the people who couldn't just get in a car and go.
Our grandparents have lived through the same evacuations, and they struggled to get themselves out. They weren't confused about the danger. Everything they were supposed to rely on failed them: the internet went down, the apps assumed you already knew how to use them, and the person who usually helps wasn't there.
They're not unusual. In the 2018 Camp Fire, which destroyed Paradise, CA, most of the 85 people who died were over 65, and many had mobility or health limitations. Evacuation plans quietly assume three things: a car, a working phone network, and someone to help. When a fire arrives, cell towers burn or saturate, power goes out, Maps has no signal, and 911 is overwhelmed.
We call the gap between the evacuation order and the fire front the margin: the hour when no one is coming. We built Margin to give the people most likely to be left behind a plan for that hour, with nothing but the phone in their pocket.
What it does
Margin is an offline, on-device wildfire evacuation planner and navigator for older adults, wheelchair and walker users, and people with low vision, plus the caregivers and county or nonprofit staff who prepare them before fire season.
Before fire season, staff prepare each person:
- Import a client list (name, address, mobility profile, language, caregiver).
- Margin builds a county map from real public data: streets, slopes, surfaces, steps, curb cuts, refuges, and fire history. For each person, it finds two real nearby refuges they can physically reach, for example "the parking lot on Nunneley Road".
- Staff review the plan on a map, sign off on it, and print a large-print two-page evacuation card with a QR code. If the phone dies, the card still works.
- Every sentence the phone might ever say to that person is pre-recorded in one calm, consistent voice, including reroutes onto any street within 1 km of their routes.
During a fire, the person (or a caregiver) uses the iPhone app:
- Scan the card's QR code once. From then on, everything lives on the phone. Airplane mode is the expected operating condition.
- Point the phone at the smoke. The compass turns "where is the fire" into a direction, with no data needed.
- Pick one of two refuges, each shown with a big map and the travel time at your speed, read aloud.
- Navigate with large instructions, spoken cues, haptics, and GPS progress.
- Say "blocked" and Margin closes just that street and detours while keeping your direction. Say "fire ahead" and it turns you away from the fire and re-ranks the refuges. A tree down and a fire are different problems, so they get different responses.
- Other spoken commands include "repeat", "where am I", "louder", "slower", and "how much further".
Everything degrades gracefully. With no GPS, Margin starts from the home address. With no compass, it skips the pointing step. With no signal, nothing changes.
How we built it
Margin has three parts that share one set of rules.
1. The engine (Python 3.12) turns public data into a routable county "bundle":
- OpenStreetMap streets for Paradise, CA: 10,412 nodes and 10,992 edges
- USGS 3DEP elevation for the slope of every street segment
- LANDFIRE fuel data, NWS wind, and 8 historical CAL FIRE perimeters (including the 153,336-acre Camp Fire)
- 32 real refuges found and scored from OSM; 26 unsafe or dead-end candidates excluded
- A mobility-aware router with five profiles: manual chair, pushed chair, power chair, walker/cane, and bedbound (shelter-in-place)
Each profile is a set of hard limits. A manual wheelchair user, for example, can't take grades above 8%, steps, unpaved paths, or curbs without cuts. Any street that breaks a limit is removed from that person's map, and every remaining segment $e$ costs travel time at the person's own speed:
$$ t_e = \frac{\ell_e}{v_p \cdot s(g_e)} $$
Here $\ell_e$ is the segment length, $v_p$ is the profile's speed (1.2 m/s for a manual chair, 0.6 m/s for a walker), and $s(g_e)$ is an uphill slowdown factor based on the grade. That's why the minutes on Harold's card are his minutes, not a driver's.
2. The staff tool (FastAPI + HTMX + SQLite) covers import, planning, map review, sign-off, PDF card printing, QR codes, and voice pre-generation with Cartesia. A volunteer with a spreadsheet of 200 clients should never hit a dead end.
3. The iPhone app (Swift/SwiftUI, iOS 16+) is built on EmberCore, a pure-logic Swift port of the Python router. Parity tests guarantee both sides produce the same route and the same spoken text. Map tiles, streets, refuges, and the latest fire and wind data are downloaded to the phone while it still has signal. After that, Margin uses only what’s on the device, so the map, the route, and the reroutes all work in airplane mode.
One rule holds the system together: any sentence the app can say must exist in a shared phrase file and be pre-recorded by the staff tool. Otherwise it's a bug, and the tests catch it.
Where Claude fits, and where it doesn't. We use Claude for three jobs:
- Reviewing refuge imagery. It can only lower a refuge's score, never raise it.
- Writing plan text. Code injects every name and number, and a back-translation check verifies the result.
- An optional online voice assistant. It reads back every action and waits for a "yes" before acting.
Remove the API key and Margin still routes, prints, and speaks. The safety path is fully deterministic.
Claude reviews refuge imagery (it can only lower a score), writes the plain-language plan text with every name and number injected by code, and runs an optional online voice assistant that confirms before acting; it never picks a route.
Cartesia pre-records every sentence Margin could say to a person, in one calm voice, so the phone speaks naturally in airplane mode.
Apify runs the scrapers that pull survivor accounts of past fires and live county road closures and zone statuses, data no API provides, to validate our refuges and keep the bundle current before it goes offline.
Demo: Harold Kim is a sample client in a manual wheelchair on Buschmann Road in Paradise. His route goes Buschmann Rd → Recreation Dr → Pearson Rd → Clark Rd → the parking lot on Nunneley Road. We checked it against USGS aerial imagery to confirm it follows real roads. His bundle holds 9,877 pre-recorded clips with 0 failures.
Challenges we ran into
Making it completely offline. This was the hardest part, and it shaped everything. Routing, rerouting, speech, the compass, and the map all had to run on the phone with zero signal.
- Speech: Text-to-speech generated on the fly either needs the internet or falls back to a robotic system voice, which is the wrong voice for a frightened 80-year-old. So we pre-record everything. Then we found that covering every street in the county would take 22,000 clips per person, so we capped reroute coverage at a 1 km radius around the planned routes. A QA pass still found 226 sentences that could have fallen back to the system voice. We pre-recorded all of them.
- Getting the data onto the phone: A real county bundle is about 400 MB. Moving it, plus signed builds, onto a physical iPhone through a personal Apple developer account was a project in itself.
Rerouting around blocked roads and fire. Recalculating on the phone in real time had to be fast, correct, and never strand anyone.
- A downed tree and a fire need opposite behavior. A blockage closes one street and keeps you heading the same way. A fire closes a wide wedge of streets toward it and turns you away. We built and tested them as separate systems with separate spoken messages.
- We fuzz-tested the router with random blockages and fire wedges. It must always return either a route or an explicit shelter-in-place fallback, never nothing.
Real data is messy.
- The OpenStreetMap API kept timing out, so we wrote our own converter from a raw Geofabrik extract.
- Our first demo used a synthetic street grid laid over real satellite imagery, and the routes "cut through grass". We rebuilt everything on real roads and checked the routes against aerial photos.
- Real routes were longer than our test routes, so the two-page card overflowed. We tightened the layout without shrinking the accessibility fonts.
Accomplishments that we're proud of
- It works in airplane mode on a real iPhone: QR scan, voice clips, voice commands, and blocked-road and fire reroutes.
- 197 automated tests: 74 for the engine, 53 for the staff tool, and 70 for the app. These include Python↔Swift parity tests and "every button in every order" stress tests.
- Accessibility treated as hard requirements: text never truncates at the largest font size, contrast is at least 7:1, touch targets are 56–64 pt, and every screen works with VoiceOver.
- Built entirely on real public data. No Google imagery is cached, because its terms forbid it.
What we learned
- Offline-first is a design philosophy, not a feature. Every decision, from voice to maps to rerouting, had to be made assuming no signal.
- Accessibility is about constraints, not polish. An 11% grade isn't a slower route for a wheelchair user. It's no route at all.
- AI belongs at the edges of safety-critical software. We let Claude help where a mistake is harmless and fenced it out of every decision that could send someone the wrong way.
- The printed card matters. When everything else fails, paper still works.
What's next for Margin
- Stitch street names from audio fragments to cut the clip count by about 10×
- Caregiver voice cloning, so the voice giving directions is a familiar one (the consent flow already exists)
- Spanish and Hmong cards and audio
- Any county in minutes: draw a map region, get a bundle
- Integration with county alert systems (Genasys/Zonehaven) for automatic "leave now" alerts by zone
- A field trial with a Northern California independent-living center
Built With
- apple-speech-framework
- avfoundation
- core-location
- fastapi
- htmx
- mapkit
- playwright
- pyosmium
- qrcode
- reportlab
- sqlite
- swiftui
- xcodegen
- xctest
Log in or sign up for Devpost to join the conversation.