Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SafeRoute

A safety-scored navigation app built on the Offendersearch API.

Enter a start and a destination. SafeRoute pulls several driving alternatives, samples points every ~0.5 mi along each, checks how many registered sex offenders live near each point, and turns that into a 0–100 safety score per route — so you can see the tradeoff and pick "a few minutes slower, much safer." Routes show as ranked cards (Waze-style), color-graded on a full-screen dark map, with a live counter of how many registries are being searched.

Coverage spans all 58 US sex-offender registries — every US state, DC and the territories — behind one API call per sampled point.

Not a consumer report. SafeRoute reports aggregate public-record counts for personal awareness and safety only. It is not for FCRA-covered decisions (housing, employment, credit, insurance) and must never be used to harass, target, or make decisions about any individual.

SafeRoute screenshot placeholder


What's in the box

Piece Tech Key needed?
Map + basemap Leaflet + OpenStreetMap / CARTO tiles No
Geocoding (address → lat/lng) Nominatim No
Driving route OSRM public demo server No
Offender counts Offendersearch API Yes (free)

The only keyed call is the offender lookup, and it happens on a tiny Node/Express backend so your API key never reaches the browser.

This app runs on the Offendersearch API. The safety score is the Offendersearch data — without a key there are no counts, no scores, and no ranking. SafeRoute talks to the API's exact request and response shape directly (no generic "data provider" layer); it's built to be the shortest path from git clone to a working Offendersearch integration you can extend.


Get a free API key

Get a free key at offendersearch.app — the free tier includes 25 searches to start, enough to try SafeRoute on a few routes (see the API-call budget below). Need more? Grab a key with higher quota from the dashboard. The app requires this key to function.


Setup

# 1. Clone / fork, then:
cd examples/safe-route
npm install

# 2. Add your key
cp .env.example .env
#   then edit .env and set:
#   OFFENDERSEARCH_API_KEY=os_live_xxxxxxxxxxxxxxxx

# 3. Run
npm start

Open http://localhost:3000.

The app reads the key from the OFFENDERSEARCH_API_KEY environment variable only — there is no key in the source, and .env is gitignored. Never commit a key.


Using the Offendersearch API

This is the whole integration — one endpoint, one request shape, one response shape. SafeRoute talks to it directly (no wrapper/abstraction), so this section doubles as a complete, copy-pasteable reference for your own code.

Endpoint & auth

POST https://api.offendersearch.app/v1/search
X-API-Key: <your key>          # from env; get one at https://offendersearch.app
Content-Type: application/json

Request body

A single radius search around a point. Note: perPage goes inside query.

{
  "query": {
    "lat": 29.76,
    "lng": -95.37,
    "radiusMiles": 1,
    "perPage": 1
  }
}
Field Type Notes
query.lat / query.lng number Center of the search.
query.radiusMiles number Search radius in miles.
query.perPage number Records per page. Use 1 when you only need the count — it keeps responses tiny and, for SafeRoute, avoids pulling individual records at all.

Response body

{
  "searchId": "srch_…",
  "status": "complete",
  "counts": {
    "records": 287,          // ← total offenders within the radius
    "sourcesComplete": 58,   // ← all 58 US registries answered
    "sourcesQueried": 58
  },
  "records": [
    { "addresses": [ { "lat": 29.75, "lng": -95.36 } ] }
  ]
}
Field Type What SafeRoute does with it
counts.records number The scoring input. Total registrants within the radius — no paging needed.
counts.sourcesComplete number How many of the 58 registries answered (coverage check).
records[].addresses[].lat / .lng number Per-record coordinates. SafeRoute does not use these — it plots aggregate density only, never individual home locations.

Minimal integration (exactly what SafeRoute's backend does)

const res = await fetch("https://api.offendersearch.app/v1/search", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": process.env.OFFENDERSEARCH_API_KEY,
  },
  body: JSON.stringify({ query: { lat, lng, radiusMiles: 1, perPage: 1 } }),
});
const data = await res.json();
const offendersNearby = data.counts.records; // that's the whole thing

That counts.records integer is the entire safety signal. Everything else in this repo — sampling, alternatives, weighting, the map — is presentation built on top of that one number. Full reference: offendersearch.app/docs.


How scoring works

For each sample point along each route, the backend calls the Offendersearch /v1/search endpoint and reads counts.records — the total number of registered offenders within the search radius. It never pages through the individual records (privacy + speed): one call returns the count it needs.

  1. Get route alternatives. OSRM is asked for up to 3 driving routes (alternatives=3) between the two points.

  2. Sample each route. Every route polyline is divided into points spaced ~0.5 mi apart (5–26 per route).

  3. Count nearby offenders. For each point, query offenders within a small radius (default 1 mile). Points that overlap between alternatives (shared start/end corridors) are deduped by rounded coordinates so they're queried once, and progress is streamed to the UI as a live call count.

  4. Score each point. A raw count c becomes a 0–100 point score on a logarithmic curve, so the first few offenders matter more than the 40th:

    pointScore = 100 × (1 − min(1, ln(1 + c) / ln(1 + SATURATION_COUNT)))
    

    with SATURATION_COUNT = 100 (a point near ~100 offenders scores ≈ 0).

  5. Blend into a route score. The overall score leans on the average point score but keeps weight on the single worst point, so one dangerous stretch can't hide behind many quiet ones:

    overall = 0.7 × mean(pointScores) + 0.3 × min(pointScores)
    
  6. Weight it — registry proximity is one optional signal, not a verdict. A 4-way control lets the user decide how much the registry layer counts:

    Mode Effect on score
    Off Registry not factored in, not shown
    Show only Marked on the map, but not scored
    Prefer avoiding (default) Moderate penalty (weight 0.5)
    Strongly avoid Full penalty (weight 1.0)

    The weight w transforms each route's registry-only score: weighted = 100 − w × (100 − registryScore). Because that's affine, changing the toggle recomputes ranking instantly on the client with zero new API calls. A road near registrants is never labeled "dangerous" — it's registry proximity, one input the user chooses to weigh.

  7. Visualize. The chosen route is drawn segment-by-segment, colored by the worse of its two endpoints (green → yellow → orange → red); other alternatives stay dimmed in their own colors. A sized dot at each sample point shows relative density. Only aggregate counts are ever plotted — never an individual offender's home location.

Both knobs (SATURATION_COUNT, the 0.3 worst-point weight) live at the top of server.js — tune them to taste.

Score bands

Score Meaning
80–100 Low exposure
60–79 Moderate exposure
40–59 Elevated exposure
0–39 High exposure

API-call budget

SafeRoute scores every alternative at ~1 call per sampled point, and sampling every ~0.5 mi means a lot of points — this is the showcase. It's what a real Offendersearch integration looks like under load.

Scoring 3 alternatives ≈ 40–90 API calls for a typical metro trip (each route is sampled densely — roughly every 0.3 mi, 8–40 points per route — so the safety profile is fine-grained). The live loading counter shows the running total as it works, and the card header reports exactly how many calls the run used.

This app is a showcase of the Offendersearch API, so it deliberately keeps the call volume high:

  • Minimal caching. Counts are cached only briefly (~45 s) and only to dedupe points within a single scoring pass — every new session re-queries the live API.
  • Dense sampling. ~1 sample every 0.3 mi (8–40 points/route) for a detailed per-segment safety profile.

Each call is a real query against every US registry — so a heavier trip means a richer picture. For real use, get a key with more quota at offendersearch.app (the free tier is 25 searches).

On the free 25-search tier that's roughly one full 3-route scoring pass — get a key with more quota at offendersearch.app for real use. That heavy usage is the point: it's a live demo of what the API does at scale.


Extending to criminal records — same API, same shape

SafeRoute scores on sex-offender registry data only today. The criminal layer is the same Offendersearch API — not a third party. When the Offendersearch criminal-records scope launches, you add a second call to the same POST /v1/search endpoint with a criminal scope and blend its counts.records into the same per-segment penalty. One API, one data structure, one key powers every safety layer. No FBI feed, no city portal, no SpotCrime — everything stays on Offendersearch infrastructure:

  1. Add a second call to the same endpoint. In offenderCount() in server.js, issue a parallel POST /v1/search with the criminal scope — identical { query: { lat, lng, radiusMiles, perPage: 1 } } body, identical counts.records in the response. Same key, same client. Keep it a separate cache key so the two scopes cache independently.

  2. Score each independently. Run the existing pointScore() on each counts.records to get registryScore and criminalScore per point.

  3. Blend into the same penalty. Combine them with a weight you choose, e.g.:

    const blended = 0.5 * registryScore + 0.5 * criminalScore;

    Then feed blended into the same mean + worst-point aggregation and the same registry-proximity weighting the UI already exposes.

  4. Surface it in the UI. Extend the existing weight control (registry / criminal / combined). The map coloring already keys off the point score, so it needs no change.

Nothing else moves — routing, sampling, caching, streaming, and visualization all stay the same, because it's the same request/response shape you already speak. See the API reference at offendersearch.app/docs.


The real product: safety-weighted routing

This demo scores route alternatives that a router already produced — a pragmatic approach that works with any routing service and shows the tradeoff clearly. But it's only step one.

The production approach is edge-costing. A router like Valhalla on OpenStreetMap data supports dynamic, per-edge (per-road-segment) costing. Instead of scoring a handful of finished routes, you assign every road segment a cost and let the router generate the lowest-cost path from the ground up:

edge_cost = travel_time
          + registry_penalty        // Offendersearch offender-proximity layer
          + crime_penalty           // future: Offendersearch criminal scope
          + lighting_penalty        // future: nighttime / street-lighting layer
          + isolation_penalty       // future: low-foot-traffic / seclusion layer

In that model:

  • Registry proximity is one weighted layer among several — exactly the framing of the toggle in this demo, but applied at the edge level so the router can route around a hotspot instead of just reporting it.
  • The Offendersearch API is the offender-proximity layer (and, as above, the criminal-records layer too) — the same /v1/search counts, precomputed per edge or fetched along candidate corridors and cached.
  • Every safety signal is a weight the user controls, never a verdict. The router optimizes a blended cost; the human decides how much each layer matters.

SafeRoute is the on-ramp to that: it proves the Offendersearch data is the safety signal, in a form you can fork today and grow into full edge-costed routing.


Swapping components

  • Google Maps instead of Leaflet/OSM. SafeRoute uses Leaflet + OpenStreetMap so it runs with zero keys and zero cost. To use Google Maps, swap the map init in public/app.js for the Google Maps JS SDK, replace getRoute() with the Directions API, and geocode() with the Geocoding API (both need a Google API key). The scoring backend is unchanged — it only cares about a list of {lat, lng} points.
  • Different routing/geocoding. Any service that yields a lat/lng list works; sampleAlong() and /api/score-routes don't care where the polyline came from.

Project structure

safe-route/
├── server.js            Express proxy + scoring (hides the API key, caches, streams)
├── package.json
├── .env.example         Copy to .env and add OFFENDERSEARCH_API_KEY
├── LICENSE              MIT
└── public/
    ├── index.html       Navigation-style single-page UI
    ├── styles.css       Styling
    └── app.js           Geocode → alternatives → sample → score → rank → visualize

Endpoints (this app's backend)

Method Path Purpose
POST /api/score-routes Body { routes: [{ points: [{lat,lng}] }], radiusMiles }. Dedupes points shared across routes, then streams newline-delimited JSON: start → many progress (live call count) → result (per-route counts + blended score)
GET /api/health { ok, hasKey } — used to warn if no key is configured

The upstream Offendersearch call it proxies:

curl -X POST https://api.offendersearch.app/v1/search \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": {"lat": 29.76, "lng": -95.37, "radiusMiles": 1, "perPage": 1}}'
# → { "counts": { "records": 287, "sourcesComplete": 58, ... }, "records": [...] }

Disclaimer

SafeRoute presents aggregate counts of public-record registry data for personal awareness and safety only. It is not a consumer report, is not governed by and must not be used for any purpose under the Fair Credit Reporting Act (FCRA) — including housing, employment, credit, or insurance decisions — and must never be used to identify, harass, target, or make decisions about any individual. Registry data may be incomplete or out of date; verify anything important with the official source.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages