Response-compatible with Google's legacy Distance Matrix API, plus two extra arrays exposing per-endpoint geocode confidence.
GET https://open-distance.com/maps/api/distancematrix/json
?origins=<A>|<B>|...
&destinations=<C>|<D>|...
&units=imperial|metric # default imperial
&mode=driving # only mode supported
Each origin/destination is either an address string or a lat,lng
pair. Multiple endpoints separated by |. Up to 100 elements
(origins × destinations) per request.
{
"destination_addresses": ["…canonical or raw input…"],
"destination_matches": ["rooftop" | "interpolated" | "coords" | ""],
"origin_addresses": ["…"],
"origin_matches": ["…"],
"rows": [
{ "elements": [
{ "status": "OK" | "NOT_FOUND" | "ZERO_RESULTS",
"distance": { "text": "5.4 mi", "value": 8690 },
"duration": { "text": "11 mins", "value": 660 } }
] }
],
"status": "OK"
}
Each endpoint comes back with a match value telling you how
it was located:
| Value | Means | Typical accuracy |
|---|---|---|
rooftop | Exact mapped point (NAD or OpenAddresses rooftop dataset) | Building-level |
interpolated | OSM addr-tagged node, or TIGER segment interpolation | ~30–100 m |
coords | Caller-supplied lat,lng directly | Whatever the caller gave us |
"" | Geocode failed; raw input is echoed | Element returns NOT_FOUND |
Successful responses send Cache-Control: public, max-age=3600,
so Cloudflare's edge cache absorbs identical queries for an hour. The
/coverage endpoint sends max-age=86400. You're
welcome to cache responses in your own backend for as long as you like.
Per-IP rate limits on the hosted deployment:
There is no paid tier. The hosted instance is intentionally
a free shared resource for casual use. If you need higher limits, self-host
on your own Cloudflare account — limits are env-var configurable per tier,
or set all three to 0 for an unlimited deployment.
Every API response (both 200 success and 429 rate-limited)
carries the following headers so you can self-throttle without a probe
round-trip:
| Header | What it means |
|---|---|
X-RateLimit-Limit-Second | Configured per-second limit (25 by default) |
X-RateLimit-Remaining-Second | Requests left in the current 1-second window |
X-RateLimit-Reset-Second | Seconds until the 1-second window rolls (always 1) |
X-RateLimit-Limit-Hour | Configured per-hour limit (500 by default) |
X-RateLimit-Remaining-Hour | Requests left in the current hour bucket |
X-RateLimit-Reset-Hour | Seconds until the hour bucket rolls |
X-RateLimit-Limit-Day | Configured per-day limit (10,000 by default) |
X-RateLimit-Remaining-Day | Requests left in the current day bucket |
X-RateLimit-Reset-Day | Seconds until the day bucket rolls |
RateLimit-Limit | IETF draft header: limit of the tightest tier |
RateLimit-Remaining | Remaining of the tightest tier |
RateLimit-Reset | Seconds until the tightest tier resets |
You get HTTP 429 with "status":"OVER_QUERY_LIMIT",
a Retry-After header (seconds until the offending tier rolls),
and X-RateLimit-Tier = sec / hour /
day indicating which bucket overflowed. The response body
includes a link back to the source so you can self-host with your own
limits.
fare, duration_in_traffic, geocoded_waypoints, copyrights, or warnings fields.place_id: inputs return NOT_FOUND.mode=driving is supported.match arrays added (the legacy schema ignores unknown fields, so existing clients are unaffected).NOT_FOUND rather than confidently-wrong distances.| Path | What it returns |
|---|---|
/healthz | Liveness + sentinel-tile probe |
/coverage | Version, data sources, supported match values |
The whole stack (Cloudflare Worker + R2 + D1 + KV) costs ~$5–10/month for
the entire continental US. Source: github.com/kurtpayne/open-distance.
./refresh.sh handles the data pipeline from a clean clone.