Routes, stops and corridors
Exact counts along a route, per leg and per stop, with nameless pins.
Livehttps://api.offendersearch.appSandboxhttps://sandbox.offendersearch.appRoute search: who is registered along a route
POST /v1/map/route answers who is registered along a route, exactly: within a corridor either side of a line, within a radius around each stop, or both. The answer counts each leg and each stop, and places every pin on the route. It shows registered addresses, not where anyone is.
curl -X POST https://api.offendersearch.app/v1/map/route \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"profile": "bus",
"polyline": "o|{rFnuqyN_v@oi@o}@g^",
"stops": [
{"id": "stop-1", "lng": -82.9988, "lat": 39.9612},
{"id": "stop-2", "lng": -82.9920, "lat": 39.9700},
{"id": "stop-3", "lng": -82.9870, "lat": 39.9800}
],
"corridorMiles": 0.1,
"stopRadiusFeet": 1000,
"externalId": "route-7-AM",
"sandbox": true
}'A route answer gives exact counts per leg and per stop, with pins and distances but no names. The full record behind a pin is opened, and billed, one at a time.
- Exact, never estimated. Only the map tiles the corridor and the stop radii touch are read, and every address in them is measured. A long diagonal route reads a strip, not its bounding box.
- One call for many routes.
POST /v1/map/routestakes up to 50, all or nothing. See the batch route. - Test mode works here too. Send
"sandbox": true; the example above has people around it.
Who it is for, with pricing worked out for a district and a walking app: the sex offender route API page.
Route path formats: GeoJSON, points and polylines
Send the line in the form your routing tool already gives you. Send at most one of path, points and polyline.
| Field | Format | Where it comes from |
|---|---|---|
path | A GeoJSON LineString, or a GeoJSON Feature holding one | A drawn line, a GeoJSON file, turf.js |
path or points | [[lng, lat], …], longitude first | Any list of coordinates |
polyline | An encoded polyline, precision 5 (the default) | Google Directions: routes[0].overview_polyline.points |
polyline with "polylinePrecision": 6 | An encoded polyline, precision 6 | Mapbox Directions with geometries=polyline6, OSRM and Valhalla |
stops only | Two or more stops and no line | The stops are joined by straight lines |
- A line may have up to 50,000 points. One with more than 2,000 is simplified to within about 5 metres first, so a turn-by-turn line from a directions service fits.
- A polyline read at the wrong precision lands far from where it was drawn. It is refused with
invalid_polyline, never answered somewhere else. - A route may not cross the date line or span half the globe.
Route corridor and bus stop radius
measure | Counts | Needs |
|---|---|---|
corridor | Everyone whose registered address is within corridorMiles of any part of the line (default 0.1 miles either side). | A line, or two or more stops. |
stops | Everyone within stopRadiusFeet of each stop, plus any parcelAllowanceFeet. | One or more stops. |
both | Both, with one total of distinct people. | A line or two stops, and one or more stops. |
Leave measure out and it follows what you sent: both when stopRadiusFeet is sent (stops when there is no line), otherwise corridor.
profileiswalk(the default) orbus. It sets the limits and the default stop radius: 500 feet walking, 1,000 feet by bus.parcelAllowanceFeetadds 0 to 500 feet to every stop radius. A registered address is one geocoded point; the allowance stands in for the distance from that point to the property line.- Stops are in route order. The line is cut into legs at the stops, and each stop is placed along the line in that order, so a loop that ends where it starts puts its last stop at the end.
- Stop ids are yours. Answers name a stop by its
idonly. Alabelis accepted and never echoed.
Route stop fields in a request: the MapRouteStopInput object
One stop in stops[] of a route request. A bare [lng, lat] pair is accepted too, and gets the id s1, s2, … by its position.
Returned or accepted by POST /v1/map/route, POST /v1/map/routes.
| Field | Type | Description |
|---|---|---|
| id | string | Your id for the stop, up to 64 characters, unique within the route. Left out: s1, s2, … by position. Answers name the stop by this id.Example "stop-2" |
| lng | number | Longitude of the stop, in degrees. Example -82.992 |
| lat | number | Latitude of the stop, in degrees. Example 39.97 |
| label | string | Accepted and never echoed. An answer refers to a stop by its id only, so keep your own labels on your side. |
Route legs, stops and alongMiles
{
"shape": "route",
"profile": "bus",
"measure": "both",
"detail": "full",
"datasetVersion": "2026-10-18",
"complete": true,
"lengthMiles": 1.456,
"corridorMiles": 0.1,
"stopRadiusFeet": 1000.0,
"parcelAllowanceFeet": 0.0,
"units": 3,
"tilesRead": 4,
"total": 9,
"corridorTotal": 8,
"stopsTotal": 4,
"breakdown": {"street": 9, "housed": 3, "homeless": 0, "incarcerated": 0},
"legs": [
{"leg": 1, "fromStop": "stop-1", "toStop": "stop-2", "lengthMiles": 0.687, "count": 5},
{"leg": 2, "fromStop": "stop-2", "toStop": "stop-3", "lengthMiles": 0.769, "count": 3}
],
"stops": [
{"stopId": "stop-1", "alongMiles": 0.0, "count": 2, "nearestFeet": 412,
"markerIds": ["mk_Qa8…", "mk_Rt2…"]},totalis distinct people with a street address in the corridor or within a stop’s reach.corridorTotalandstopsTotalsplit it; a person can be in both.legs[]counts the corridor leg by leg. Each person is counted on the leg of their nearest point of the line, so the leg counts add up tocorridorTotal.stops[]gives each stop’scount, the distance to the closest address innearestFeet, and the pins in reach inmarkerIds.- Pins are in
markers(one person) andgroups(several people at one address), ordered along the line. In the corridor a pin hasdistanceMiles,legandalongMiles; on a route with stops it hasnearestStopandstopDistanceFeet. routeNoticesays how the route was measured. Show it next to the counts.unitsis what the route is billed as. See route units.
Route leg fields: the MapRouteLeg object
The line between two stops, in legs[]. With fewer than two stops the whole line is one leg. Each person in the corridor counts on the leg of their nearest point of the line, so the leg counts add up to corridorTotal.
Returned or accepted by POST /v1/map/route, POST /v1/map/routes.
| Field | Type | Description |
|---|---|---|
| leg | integer | The leg number, from 1. Example 1 |
| fromStop | string or null | The id of the stop the leg starts at. null when the route has fewer than two stops. |
| toStop | string or null | The id of the stop the leg ends at. null when the route has fewer than two stops. |
| lengthMiles | number | The length of the leg, in miles. Line before the first stop and after the last belongs to the first and last leg. |
| count | integer | People in the corridor whose nearest point of the line is on this leg. |
Route stop fields in an answer: the MapRouteStop object
One stop, as answered, in stops[], in the order sent.
Returned or accepted by POST /v1/map/route, POST /v1/map/routes.
| Field | Type | Description |
|---|---|---|
| stopId | string | The stop's id.Example "stop-2" |
| alongMiles | number or null | Miles from the start of the line to the stop. null when there is no line. |
| count | integer or null | People within the stop's reach (stopRadiusFeet plus parcelAllowanceFeet). null when stops are not measured. |
| nearestFeet | integer or null | Feet from the stop to the closest of them. null when there is none. |
| markerIds | array of string | The marker and group ids within the stop's reach. Present when stops are measured; empty with detail=count. |
Route pin fields: the MapRouteMarker object
One person at one exact street address near the route, in markers[]. A position, its place on the route and an id. Never a name, on any plan.
Returned or accepted by POST /v1/map/route, POST /v1/map/routes.
| Field | Type | Description |
|---|---|---|
| id | string | The marker id, mk_…. Pass it to GET /v1/map/records/{markerId} to open the record (one record open). It works for 24 hours, only for the account it was issued to. |
| lat | number | Latitude of the registered address, in degrees. |
| lng | number | Longitude of the registered address, in degrees. |
| precision | string one of "street" | Always street. |
| addressType | string one of "home", "work", "school", "other" | Which registered address this pin is. |
| distanceMiles | number | Straight-line miles from the registered address to the nearest point of the line. Present when the pin is in the corridor. Example 0.04 |
| leg | integer | The leg (from 1) the nearest point of the line is on. Present when the pin is in the corridor. Example 1 |
| alongMiles | number | Miles from the start of the line to its nearest point. Present when the pin is in the corridor. Example 0.62 |
| nearestStop | string | The id of the closest stop. Present when the route has stops, whether or not the pin is within that stop's radius.Example "stop-2" |
| stopDistanceFeet | integer | Straight-line feet from the registered address to that stop. Present when the route has stops. Example 412 |
Route group pin fields: the MapRouteGroup object
Several people registered at one exact street address near the route, as one pin in groups[]. The people behind it open with GET /v1/map/groups/{groupId} (one map request per page, no names).
Returned or accepted by POST /v1/map/route, POST /v1/map/routes.
| Field | Type | Description |
|---|---|---|
| groupId | string | The group id, mg_…, for GET /v1/map/groups/{groupId}. Works for 24 hours. |
| lat | number | Latitude of the address, in degrees. |
| lng | number | Longitude of the address, in degrees. |
| precision | string one of "street" | Always street. |
| count | integer | People registered at this address. |
| distanceMiles | number | Straight-line miles from the registered address to the nearest point of the line. Present when the pin is in the corridor. Example 0.04 |
| leg | integer | The leg (from 1) the nearest point of the line is on. Present when the pin is in the corridor. Example 1 |
| alongMiles | number | Miles from the start of the line to its nearest point. Present when the pin is in the corridor. Example 0.62 |
| nearestStop | string | The id of the closest stop. Present when the route has stops, whether or not the pin is within that stop's radius.Example "stop-2" |
| stopDistanceFeet | integer | Straight-line feet from the registered address to that stop. Present when the route has stops. Example 412 |
Approximate and unplaced people near a route
Some people are mapped only to the centre of a ZIP code, a city or a county. Their distance to the route is not known, so they are never counted in total. Those whose area centre falls in the map tiles the route touches are listed under approximate, with a count and one pin per area. Draw them so they do not look like an address.
unplaced counts people in the states the route touches who have no mappable address at all, as on a map view. Say so next to the counts, so a short list is not read as the whole list.
Route approximate-area fields: the MapRouteArea object
People mapped only to the centre of a ZIP code, city or county, in approximate.areas[]. Their distance to the route is not known, so they are listed here and never counted in total.
Returned or accepted by POST /v1/map/route, POST /v1/map/routes.
| Field | Type | Description |
|---|---|---|
| groupId | string | The group id, mg_…, for GET /v1/map/groups/{groupId}. |
| lat | number | Latitude of the area's centre. |
| lng | number | Longitude of the area's centre. |
| precision | string one of "zip", "city", "county" | What kind of area. |
| place | string | The area's name. Example "43215" |
| count | integer | People mapped to this area. |
Never partial: exact route counts or a refusal
A route is answered whole, or refused with nothing charged. There is no estimated count and no cut-off list.
| When | Answer | What to do |
|---|---|---|
More than 300 pins with detail=full | 422 route_too_many_people, with the exact counts in counts | Show the counts, send detail=count, or split the route. |
| More than 50,000 registered addresses to read | 422 route_too_dense | Split the route, or narrow the corridor. |
| A read fails | 503 map_unavailable, with no result | Retry after Retry-After. |
| One route in a batch is refused | The batch is refused, with its routeIndex | Fix or split that route and send the batch again. |
{
"detail": {
"code": "route_too_many_people",
"message": "This route has 412 places to pin, more than the 300 one response carries. …",
"counts": {
"total": 398,
"corridorTotal": 371,
"stopsTotal": 64,
"breakdown": {"street": 398, "housed": 120, "homeless": 9, "incarcerated": 0},
"legs": [{"leg": 1, "fromStop": "stop-1", "toStop": "stop-2", "lengthMiles": 0.61, "count": 12}],
"stops": [{"stopId": "stop-1", "alongMiles": 0.0, "count": 3, "nearestFeet": 288}],
"approximate": {"count": 14},
"units": 22
},
"maxMarkers": 300
}
}Route units: route corridor pricing
A route is $2.00 per 1,000 route units: 1 unit per started mile of corridor, plus 1 per 10 stops.
| Part of the route | Units | Note |
|---|---|---|
| The corridor, when it is measured | 1 unit for each started mile of line | Every corridor accepted, up to 0.25 miles either side, counts ×1. |
| The stops, when a stop radius is measured | 1 unit for each started 10 stops | Stops sent but not measured add nothing. |
| Every route | At least 1 unit | A short walk with no stops is 1 unit. |
Worked example. A 17.42-mile bus route with a 0.1-mile corridor and 35 stops at 1,000 feet is 18 + 4 = 22 units, $0.044. Checking 300 such routes in each of 3 terms is 19,800 units, $39.60 a year in map requests.
detail=countcosts the same as a route with pins.- A batch costs the sum of its routes, and nothing unless the whole batch answers.
- The same route asked again with
If-None-Matchanswers304, free. Test mode and errors are free. - Opening a record behind a pin is one record open, $0.035 by default. See Billing.
No names on a route answer
A route answer carries positions, distances, counts and ids. It never carries a name, on any plan, and the markerFields: named plan setting does not apply to it.
The reason is the shape. A map view is bounded by a screen, and names start only from zoom 15, a few streets across. A route can run for 60 miles, so a named answer would be a list of names along a whole county. The record behind any pin is still one call away: pass its id to GET /v1/map/records/{markerId}, one record open.
For a map with name labels, request GET /v1/map/viewport at zoom 15 or closer for the part of the route on screen. That is an ordinary map view, one map request, with named pins.
Route limits for walk and bus profiles
| Limit | walk | bus | Past it |
|---|---|---|---|
| Longest line | 15 miles | 60 miles | 422 route_too_long |
| Stops | Up to 50 | Up to 150 | 422 route_too_many_stops |
Corridor (corridorMiles) | 0.02 to 0.25 miles, default 0.1 | 0.02 to 0.25 miles, default 0.1 | 422 invalid_corridor |
Stop radius (stopRadiusFeet) | 100 to 2,640 feet, default 500 | 100 to 2,640 feet, default 1,000 | 422 invalid_radius |
| Parcel allowance | 0 to 500 feet | 0 to 500 feet | 422 invalid_radius |
| Points in the line | 50,000 sent; 2,000 after simplifying to about 5 metres | The same | 422 route_too_many_points |
Pins in a detail=full answer | 300, counting markers, groups and approximate areas | The same | 422 route_too_many_people, with the exact counts |
| Registered addresses one route reads | 50,000 | The same | 422 route_too_dense |
| Routes in one batch | 50 | 50 | 422 route_batch_too_large |
GET /v1/map/status returns these under routes.profiles. An end user may check 20 different routes a day by default; see Daily limits.
Route profile limits: the MapRouteProfile object
The limits of one route profile, as GET /v1/map/status reports them under routes.profiles.
Returned or accepted by GET /v1/map/status.
| Field | Type | Description |
|---|---|---|
| maxMiles | number | The longest line, in miles. Example 60 |
| maxStops | integer | The most stops. Example 150 |
| corridorMiles | array of number | The narrowest and widest corridor, in miles either side of the line. Example [0.02,0.25] |
| stopRadiusFeet | array of number | The smallest and largest stop radius, in feet. Example [100,2640] |
Route API samples: curl, Node and Python
curl -X POST https://api.offendersearch.app/v1/map/route \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"profile": "bus",
"polyline": "o|{rFnuqyN_v@oi@o}@g^",
"stops": [
{"id": "stop-1", "lng": -82.9988, "lat": 39.9612},
{"id": "stop-2", "lng": -82.9920, "lat": 39.9700},
{"id": "stop-3", "lng": -82.9870, "lat": 39.9800}
],
"corridorMiles": 0.1,
"stopRadiusFeet": 1000,
"externalId": "route-7-AM",
"sandbox": true
}'Sex offender map API: POST /v1/map/route request and response fields
Exact counts and nameless pins along one route: within a corridor either side of a line, within a radius of each stop, or both, per leg and per stop. Billed as route units, each one map request; detail=count costs the same; a 304, test mode and errors are free. Never partial: a route is answered whole or refused with nothing charged.
Request
| Field | Type | Description |
|---|---|---|
| X-API-Key required | string | A secret API key (from your server), or a map browser key os_map_pk_… sent from a site on that key's list. A dashboard session can send Authorization: Bearer <token> instead. |
| X-End-User optional | string | Any stable id you use for the person behind this request, such as a hashed user id. Required with a map browser key (422 end_user_required); optional with a secret key. Pins are counted against that user's daily area budget (2,000 distinct areas), and each different route against their daily route limit (20 by default). |
| If-None-Match optional | string | The ETag of an earlier answer for the same route. Unchanged, it answers 304 with no body and is not billed. |
| Field | Type | Description |
|---|---|---|
| path optional | object or array | The line: a GeoJSON LineString, a GeoJSON Feature holding one, or a list of [lng, lat] points. Up to 50,000 points; a line with more than 2,000 is simplified to within about 5 metres first.Example {"type":"LineString","coordinates":[[-83.0006,39.9601],[-82.9951,39.9608]]} |
| points optional | array of array | The line as [[lng, lat], …], longitude first: the same list POST /v1/map/area takes.Example [[-83.0006,39.9601],[-82.9951,39.9608]] |
| polyline optional | string | The line as an encoded polyline, exactly as a directions service returns it: Google Directions overview_polyline.points (precision 5), or a Mapbox Directions geometries=polyline6 geometry, OSRM or Valhalla (precision 6).Example "o|{rFnuqyN_v@oi@o}@g^" |
| polylinePrecision optional · default 5 | integer one of 5, 6 | The precision of polyline: 5 for Google, 6 for Mapbox, OSRM and Valhalla. Read at the wrong precision a line lands far away and is refused with invalid_polyline. |
| stops optional | array of MapRouteStopInput | The stops, in route order: {id, lng, lat} objects, or [lng, lat] pairs. Walk: up to 50. Bus: up to 150. The line is cut into legs at the stops. |
| profile optional · default "walk" | string one of "walk", "bus" | Sets the limits and the default stop radius. walk: up to 15 miles, 50 stops, 500 feet. bus: up to 60 miles, 150 stops, 1,000 feet. |
| measure optional | string one of "corridor", "stops", "both" | What is counted: the corridor along the line, a radius around each stop, or both. Left out: both when stopRadiusFeet is sent (stops when there is no line), otherwise corridor. |
| corridorMiles optional · default 0.1 | number | The distance either side of the line, 0.02 to 0.25 miles. |
| stopRadiusFeet optional | number | The radius around each stop, 100 to 2,640 feet. Left out: 500 for walk, 1,000 for bus.Example 1000 |
| parcelAllowanceFeet optional · default 0 | number | 0 to 500 feet added to the stop radius. A registered address is a geocoded point; an allowance stands in for the distance from that point to the property line. |
| detail optional · default "full" | string one of "full", "count" | full for counts and nameless pins, count for counts only (no pins and no ids). The same price. |
| format optional · default "json" | string one of "json", "geojson" | json for the MapRouteResponse shape, geojson for a MapGeoJson FeatureCollection. A batch answers in json only. |
| housing optional | string or array of string | Keep only people with these housing statuses: a comma-separated string or a list of housed, homeless, incarcerated.Example ["homeless"] |
| precision optional | string one of "street", "any" | street leaves out the people counted under approximate. any, or leaving it out, keeps everyone. |
| sandbox optional · default false | boolean | true answers in test mode: made-up people, never billed. |
| externalId optional | string | Your own id for the route, up to 100 characters, echoed in the answer. Example "route-7-AM" |
Response
The answer for this route, with Cache-Control: private, max-age=300 and an ETag. With format: "geojson" the body is a MapGeoJson FeatureCollection.
| Field | Type | Description |
|---|---|---|
| routeIndex | integer | In a POST /v1/map/routes answer only: the route's position in the request, from 0. |
| shape | string one of "route" | Always route. |
| profile | string one of "walk", "bus" | The profile the route was answered with. |
| measure | string one of "corridor", "stops", "both" | What was counted. |
| detail | string one of "full", "count" | count answers have empty markers, groups and approximate.areas. |
| datasetVersion | string (date) | The version of the data that answered. Example "2026-10-18" |
| complete | boolean | Always true. A route is answered whole or refused. |
| lengthMiles | number | The length of the line, in miles. 0 with no line.Example 17.42 |
| corridorMiles | number or null | The corridor answered, in miles either side of the line. null when the corridor is not measured.Example 0.1 |
| stopRadiusFeet | number or null | The stop radius answered, in feet. null when stops are not measured.Example 1000 |
| parcelAllowanceFeet | number or null | The allowance added to the stop radius, in feet. null when stops are not measured. |
| units | integer | Route units: the number of map requests this route is billed as. See Route units. Example 22 |
| tilesRead | integer | The zoom-14 map tiles the corridor and the stop radii touch. |
| total | integer | Distinct people with a street address in the corridor or within a stop's reach. People under approximate are never in it. |
| corridorTotal | integer or null | People in the corridor. The leg counts add up to it. null when the corridor is not measured. |
| stopsTotal | integer or null | Distinct people within reach of any stop. null when stops are not measured. |
| breakdown | MapBreakdown | Of total: street addresses, and housing status where the registry states it. |
| legs | array of MapRouteLeg | One entry per leg. Empty when the corridor is not measured. |
| stops | array of MapRouteStop | One entry per stop, in the order sent. |
| markers | array of MapRouteMarker | One nameless pin per person at a street address, ordered along the line. Empty with detail=count. |
| groups | array of MapRouteGroup | One pin per address where several people are registered. Empty with detail=count. |
| approximate | object | People mapped only to the centre of a ZIP code, city or county inside the map tiles the route touches. Their distance to the route is not known, so they are never in total. |
| approximate.count | integer | People mapped only to an area. |
| approximate.areas | array of MapRouteArea | One pin per area. Empty with detail=count. |
| unplaced | MapUnplaced | People in the states the route touches who have no mappable address. |
| externalId | string | Your externalId, when you sent one. |
| sandbox | boolean | Present and true on a test-mode answer. |
| notice | string | A sentence to show with the answer. Example "A pin shows a registered address, not a person's current location." |
| routeNotice | string | How a route is measured, as a sentence to show next to the counts: distances are straight-line from each geocoded registered address, and a count shows registered addresses, not where anyone is. |
The same route was asked within the ETag's life. No body, not billed.
| Status | Code | When |
|---|---|---|
| 401 | — | The key is missing or invalid. The body is the plain detail string. |
| 403 | map_not_enabled | The Map API is off for this account (map_paused when we paused it, origin_not_allowed for a browser key used from a site not on its list). Send sandbox: true to build first. |
| 422 | invalid_path | The route could not be read: invalid_path, invalid_polyline, invalid_stops, invalid_profile, invalid_measure, invalid_corridor, invalid_radius, invalid_detail, invalid_format, invalid_filter. Nothing was charged. |
| 422 | route_too_long | Longer than the profile allows (route_too_many_stops for too many stops, route_too_many_points for a line still over 2,000 points after simplifying). Split the route. |
| 422 | route_too_many_people | More than 300 pins with detail=full. counts holds the exact counts; nothing was charged. Send detail=count, or split the route. |
| 422 | route_too_dense | The route passes through more registered addresses than one call counts exactly. Split it, or narrow the corridor. Nothing was charged. |
| 422 | end_user_required | A map browser key sent no X-End-User header. |
| 429 | map_end_user_route_limit | This end user has checked their daily number of different routes. A route already checked today can be checked again. map_sweep_limit and map_end_user_limit when a daily area budget is used by pins (detail=count is never refused by them); map_busy when too many map requests arrive at once. |
| 503 | map_unavailable | The route could not be read just now: no result and nothing charged. Retry after Retry-After (map_index_not_ready, map_not_configured likewise). |
Route GeoJSON output
With "format": "geojson" a single route answers as a FeatureCollection of points, content type application/geo+json: one feature per marker (kind: "marker"), group (kind: "group") and approximate area (kind: "area"). Everything else, the counts, legs and stops included, is under meta. A batch answers in JSON only.
curl -X POST https://api.offendersearch.app/v1/map/route \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"path": {
"type": "LineString",
"coordinates": [[-82.9988, 39.9612], [-82.9920, 39.9700], [-82.9870, 39.9800]]
},
"format": "geojson",
"sandbox": true
}'{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [-82.99863, 39.96133]},
"properties": {
"kind": "marker",
"id": "mk_Qa8…",
"precision": "street",
"addressType": "home",
"distanceMiles": 0.002,
"leg": 1,
"alongMiles": 0.078
}
},
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [-83.0114, 39.969]},
"properties": {
"kind": "area",
"groupId": "mg_X1G…",
"precision": "zip",
"place": "43215",
"count": 5
}
}
],
"meta": {
"shape": "route", "measure": "corridor", "units": 2, "total": 8,
"legs": [{"leg": 1, "fromStop": null, "toStop": null, "lengthMiles": 1.456, "count": 8}],
"approximate": {"count": 5, "areas": ["…"]},
"notice": "A pin shows a registered address, not a person's current location."
}
}Batch route search: up to 50 routes
POST /v1/map/routes takes up to 50 routes, each shaped like a POST /v1/map/route body, and answers them in the order sent, each with its routeIndex.
- All or nothing. One refused route refuses the batch, with
routeIndexnaming it, and nothing is billed. - One response for every route. A person near three routes is one record open when opened from this answer, and is counted once in
uniquePeople. - Billing is the sum of the routes’
units, inunits.
curl -X POST https://api.offendersearch.app/v1/map/routes \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"routes": [
{"externalId": "route-7-AM", "profile": "bus", "polyline": "o|{rFnuqyN_v@oi@o}@g^",
"stops": [[-82.9988, 39.9612], [-82.9870, 39.9800]], "stopRadiusFeet": 1000},
{"externalId": "route-7-PM", "profile": "bus",
"points": [[-82.9870, 39.9800], [-82.9988, 39.9612]], "detail": "count"}
],
"sandbox": true
}'{
"shape": "routes",
"complete": true,
"datasetVersion": "2026-10-18",
"units": 4,
"uniquePeople": 9,
"routes": [
{"routeIndex": 0, "shape": "route", "externalId": "route-7-AM", "total": 9, "…": "…"},
{"routeIndex": 1, "shape": "route", "externalId": "route-7-PM", "total": 8, "…": "…"}
],
"notice": "A pin shows a registered address, not a person's current location.",
"sandbox": true
}Sex offender map API: POST /v1/map/routes request and response fields
Up to 50 routes in one call, answered all or nothing: any refused route refuses the batch, with its routeIndex, and nothing is billed. Billed as the sum of the routes' units. Every marker shares one response, so a person near several routes is one record open.
Request
| Field | Type | Description |
|---|---|---|
| X-API-Key required | string | A secret API key (from your server), or a map browser key os_map_pk_… sent from a site on that key's list. A dashboard session can send Authorization: Bearer <token> instead. |
| X-End-User optional | string | Any stable id you use for the person behind this request, such as a hashed user id. Required with a map browser key (422 end_user_required); optional with a secret key. Pins are counted against that user's daily area budget (2,000 distinct areas), and each different route against their daily route limit (20 by default). |
| Field | Type | Description |
|---|---|---|
| routes required | array of MapRouteRequest | 1 to 50 routes, each a MapRouteRequest. format must be json or left out. |
| sandbox optional · default false | boolean | true answers every route in test mode. |
Response
Every route's answer, in the order sent.
| Field | Type | Description |
|---|---|---|
| shape | string one of "routes" | Always routes. |
| complete | boolean | Always true. A batch is answered whole or refused. |
| datasetVersion | string (date) | The version of the data that answered. |
| units | integer | The sum of the routes' units: the map requests this call is billed as. |
| uniquePeople | integer | Distinct people across every route. A person near three routes counts once. |
| routes | array of MapRouteResponse | One answer per route, each with its routeIndex. |
| sandbox | boolean | Present and true on a test-mode answer. |
| notice | string | A sentence to show with the answer. |
| Status | Code | When |
|---|---|---|
| 401 | — | The key is missing or invalid. The body is the plain detail string. |
| 403 | map_not_enabled | As on POST /v1/map/route. |
| 422 | route_batch_too_large | More than 50 routes (invalid_route for none, invalid_format for a route asking for GeoJSON). Any POST /v1/map/route refusal, with routeIndex naming the route. Nothing was charged. |
| 429 | map_end_user_route_limit | As on POST /v1/map/route. |
| 503 | map_unavailable | A route could not be read just now: no result and nothing charged. Retry after Retry-After. |
Route caching and end users
- A single route answer carries
Cache-Control: private, max-age=300and anETag. Send it back inIf-None-Match: the same route answers304, with no body, free. - Marker and group ids work for 24 hours, for your account only.
- A map browser key must send
X-End-Useron both route routes. See Platform integration. - Pins from a route count toward the daily area limit like pins from a view (
detail=countnever does), and each end user may check 20 different routes a day by default. See Limits & errors.
Route search errors
| Code | Status | Cause | What to do |
|---|---|---|---|
invalid_route | 422 | The route is not a JSON object, or a batch has no routes. | Send a route object; send 1 to 50 in a batch. |
invalid_path | 422 | More than one of path, points and polyline; a point that is not [lng, lat]; a line of one point; a corridor with no line; or a route that crosses the date line. | Send one line, longitude first. |
invalid_polyline | 422 | The polyline does not decode, or decodes off the globe. | Check polylinePrecision: 5 for Google, 6 for Mapbox, OSRM and Valhalla. |
invalid_profile | 422 | profile is not walk or bus. | Send one of the two. |
invalid_measure | 422 | measure is not corridor, stops or both. | Send one of the three, or leave it out. |
invalid_corridor | 422 | corridorMiles is outside 0.02 to 0.25. | Send a corridor inside the range. |
invalid_radius | 422 | stopRadiusFeet or parcelAllowanceFeet is outside its range. | See the limits above. |
invalid_stops | 422 | A stop is not {id, lng, lat} or [lng, lat], an id is used twice, or stops are measured with no stops. | Fix the stop, or measure the corridor only. |
invalid_detail, invalid_format, invalid_filter | 422 | As on the viewport route. A batch answers in json only. | Send an accepted value. |
route_too_long | 422 | Longer than the profile allows, or touching more map area than one call reads. | Split the route into shorter routes. |
route_too_many_points | 422 | More than 50,000 points, or more than 2,000 after simplifying. | Split the route. |
route_too_many_stops | 422 | More stops than the profile allows. | Split the route, or use bus. |
route_batch_too_large | 422 | More than 50 routes in a batch. | Send at most 50 per call. |
route_too_dense | 422 | The route passes through more registered addresses than one call counts exactly. | Split the route, or narrow the corridor. |
route_too_many_people | 422 | More than 300 pins with detail=full. counts holds the exact counts. | Show the counts, send detail=count, or split the route. |
end_user_required | 422 | A map browser key sent no X-End-User. | Send any stable id for your user, such as a hashed user id. |
map_end_user_route_limit | 429 | This end user has checked 20 different routes today (the default). | A route already checked today can be checked again. Other users are not affected. It resets at 00:00 UTC. |
map_unavailable | 503 | The route could not be read just now. No result, nothing charged. | Retry after the Retry-After header. |
Access errors (401, 403) and service errors are as on every map route. See Limits & errors.