Ooru

API

Three GET endpoints, no key, no rate limit, CORS open to everything. Encoding and decoding are pure functions over a committed word table, so every response is deterministic and cacheable forever — the routes are statically rendered and served from the edge.

GET /api/encode

Coordinates → address.

ParameterRequiredMeaning
latyesWGS84 latitude in degrees
lngyesWGS84 longitude in degrees
depthnoHow many words to return, 1–4. Defaults to 3.
curl "https://…/api/encode?lat=13.0499&lng=80.2824"

Returns the address, the per-word breakdown with each word’s level and semantic group, the cell centre in both CRSs, the point scale factor there, the footprint as GeoJSON, and the chain of containing addresses.

{ "address": "cavern.organza.panther", "display": "///cavern.organza.panther", "depth": 3, "centre": { "crs": "EPSG:4326", "lat": …, "lng": … }, "projected": { "crs": "EPSG:32644", "easting": …, "northing": …, "pointScaleFactor": … }, "cell": { "sizeMetres": 12.5, "areaSquareMetres": 156.25, "bounds": { … }, "polygon": { "type": "Polygon", … } }, "contains": [ { "address": "…", "depth": 1, … } ] }
Abridged; the real response also carries the per-word breakdown and the containment chain.

A position outside the coverage area returns 422 with error: "out_of_extent" rather than a nonsense address.

GET /api/decode

Address → coordinates. Same response shape as encode.

ParameterRequiredMeaning
addressyesThe address. Dots, spaces or slashes all work, and a leading /// is accepted.
risknoSet to include the confusion set — the plausible wrong addresses and how far each lands you.
{ "error": "unknown-word", "message": "\"rivver\" is not a Ooru word", "position": 0, "token": "rivver", "suggestions": [ { "word": "river", "level": 1, "index": …, "distance": 1 } ] }

A word in the wrong slot returns error: "wrong-level", which a flat scheme cannot detect at all — with one global vocabulary, any word is legal in any position.

GET /api/neighbours

The eight cells touching an address, with the true ground distance to each centre. This endpoint has no equivalent in a non-topological scheme.

{ "address": "cavern.organza.panther", "cellSizeMetres": 12.5, "neighbours": [ { "direction": "E", "address": "…", "centre": {…}, "distanceMetres": 12.5 }, { "direction": "NE", "address": "…", "centre": {…}, "distanceMetres": 17.678 }, … ] }
Diagonal distances come back as √2 × the orthogonal ones — a live demonstration that the tessellation really is metric and square.

/api/outages/reports

The one stateful endpoint, behind the outages screen. Live power-cut reports: a three-word address, an anonymous client token, and an expiry 10 minutes after the reporter last touched it — or the collective expiry of an outage that 3 or more networks corroborate, 15 minutes and growing as networks join; each report says whether it stands corroborated. GET returns every report still inside its cool-off; POST with { address, clientId, turnstile } creates a report or refreshes the caller’s existing one on that cell — the turnstile token comes from the site key the response carries as turnstileSiteKey, and a new report without a valid one is 403 verification_failed; DELETE with { id, clientId } ends it and deletes the row; expired rows are deleted on the next request, so nothing is kept. Every response carries the full active list. An address on the cell one of the board’s places stands on returns 409 board_place: the board’s own ground is never a home. A refresh earlier than the last 5 minutes of the countdown returns 409 too_early. Rate-limited to 3 active reports per client and 12 per network (429 rate_limited / network_limited) and to a dozen writes a minute per network and per client (429 too_many), never cached, and — unlike the three endpoints above — not part of the stable public surface: it exists to serve the screen.

Its companion POST /api/outages/contacts with { address, channel, authorityId, clientId } records that the caller reached the board from a cell that has a live report — channel is phone or whatsapp, authorityId one of the board places — one row per device, cell and channel, refreshed rather than repeated. Every response, from either endpoint, carries both lists as reports and contacts.

{ "now": 1788912345678, "coolOffMinutes": 10, "clusterCoolOffMinutes": 15, "stillOutMinutes": 5, "maxActiveReports": 3, "reports": [ { "id": "…", "address": "cavern.organza.panther", "createdAt": …, "refreshedAt": …, "expiresAt": …, "corroborated": false } ], "contacts": [] }
Only the cell and a salted hash of the caller's network are stored. The GPS fix that produced the cell never leaves the browser; the address behind the hash is never written down.

Notes for integrators

  • Everything is WGS84 on the way in. Coordinates from Survey of India toposheets are on the Everest 1830 datum and differ by 200–300 m in this region. Datum- shift before you encode — see accuracy.
  • Truncate, don’t round. To disclose a coarser position, drop trailing words rather than reducing coordinate precision. A prefix is a real address for a real, larger cell; a rounded coordinate is a point that may sit in a different cell entirely.
  • Cache freely. Responses are pure functions of their inputs and will never change for a given lexicon version. They are served with a one-year immutable cache header.
  • Coverage is finite. A 50 km × 60 km extent over the Chennai Metropolitan Area. Anything outside it is an error, deliberately, rather than a wrapped-around address.

Doing it without the network

The whole model — projection, tessellation, lexicon — is about 70 KB of committed JSON and a few hundred lines of arithmetic. For an offline or embedded client, porting it is strictly easier than calling this API, and the method page specifies it completely.