Maps & Geocoding
Render interactive maps, geocode addresses, reverse geocode coordinates, search for places, calculate routes, and let visitors draw directly on live maps.
GEOCODE — Address to Coordinates
MAP GEOCODE "10 Downing Street, London" SET ?loc
AFTER EMIT ?loc("lat") & ", " & ?loc("lon")
AFTER EMIT ?loc("house_number") & " " & ?loc("road") & ", " & ?loc("city")
MAP GEOCODE "Springfield" LIMIT 5 SET ?candidates
AFTER MAP GEOCODE "Main Street" COUNTRY "za" SET ?loc
AFTER MAP GEOCODE "Main Street" BOUNDS ?box SET ?loc
REVERSE — Coordinates to Address
MAP REVERSE LAT -33.9249 LON 18.4241 SET ?addr
AFTER EMIT ?addr("display_name")
AFTER MAP REVERSE LAT -33.9249 LON 18.4241 DETAIL "city" SET ?addr2
DETAIL controls how coarse the returned address is: "country", "region" (or "state"), "city", "road" (or "street"), and "house" (or "building"). Omitted, it resolves to the most specific address available. LNG is accepted everywhere as an alias for LON.
BOUNDARY — Administrative Region Shape
MAP BOUNDARY "South Africa" SET ?shape
SEARCH — Places of Interest
MAP SEARCH "cafe" COUNTRY "South Africa" SET ?places
AFTER MAP SEARCH "cafe" COUNTRY "za" PROVINCE "Western Cape" SET ?places2
AFTER MAP SEARCH "cafe" BOUNDS ?box SET ?places3
Search by a plain place-type and sharpen it with the COUNTRY modifier — which accepts a full country name ("South Africa") or an ISO code ("za") — optionally narrowed further with PROVINCE / STATE or a BOUNDS viewbox. LIMIT caps the number of results (default 10).
Building Points, Lines, and Shapes
Points, lines, polygons, and circles are constructed as their own values before rendering — nothing is drawn directly onto a map. A rendered map is a URL, an immutable link, so every element it displays has to be supplied to MAP RENDER as input.
MAP GEOCODE "Cape Town, South Africa" SET ?loc
AFTER MAP POINT LAT ?loc("lat") LON ?loc("lon") LABEL "Cape Town" SET ?point1
MAP POINT LAT -33.9249 LON 18.4241 LABEL "Cape Town" ICON "blue" POPUP "Open 9am–5pm" SET ?point1
AFTER MAP POINT LAT -33.9249 LON 18.4241 LABEL "HQ" PIN URL "https://mysite.com/pin.png" SET ?point2
ICON selects a named preset marker (e.g. "blue", "red"); PIN URL supplies a custom marker image from any URL. A point carries whichever it was given — both are optional, alongside LABEL and POPUP.
NEW ARRAY SET ?points
AFTER APPEND ?point1 TO ?points
AFTER APPEND ?point2 TO ?points
AFTER MAP LINE ?points SET ?routeline
MAP POLYGON ?points SET ?polygon1
MAP CIRCLE LAT -33.9249 LON 18.4241 RADIUS 5 UNIT "km" SET ?circle1
RENDER — Composing Everything Into One Map
Every element — points, lines, polygons, circles, and heatmap data — is passed to RENDER together. The result is one URL, generated once, with everything already baked in.
MAP GEOCODE "Cape Town, South Africa" SET ?loc
AFTER MAP POINT LAT ?loc("lat") LON ?loc("lon") LABEL "Cape Town" SET ?point1
AFTER NEW ARRAY SET ?points
AFTER APPEND ?point1 TO ?points
AFTER MAP RENDER ?points ZOOM 13 SET ?url
AFTER EMIT ?url
QUERY "stores" FROM "shop" SET ?rows
AFTER NEW ARRAY SET ?points
AFTER FOREACH ?rows SET ?row
OPEN
MAP POINT LAT ?row("lat") LON ?row("lon") LABEL ?row("name") SET ?pt
AFTER APPEND ?pt TO ?points
CLOSE
AFTER MAP RENDER ?points STYLE "satellite" AUTOFIT SET ?url
AFTER EMIT ?url
NEW ARRAY SET ?points
AFTER APPEND ?point1 TO ?points
AFTER NEW ARRAY SET ?lines
AFTER APPEND ?routeline TO ?lines
AFTER NEW ARRAY SET ?polygons
AFTER APPEND ?polygon1 TO ?polygons
AFTER NEW ARRAY SET ?circles
AFTER APPEND ?circle1 TO ?circles
AFTER MAP RENDER ?points
LINES ?lines
POLYGONS ?polygons
CIRCLES ?circles
HEATMAP ?places
ZOOM 13
STYLE "satellite"
AUTOFIT
SET ?url
Tile Style Presets
| Preset |
|---|
standard — default street map tiles |
satellite — Esri satellite imagery |
topo — OpenTopoMap |
toner — Stamen Toner, high-contrast black and white |
watercolor — Stamen Watercolor |
terrain — Stamen Terrain |
light — CartoDB Positron |
dark — CartoDB Dark Matter |
Output Form — AS "html" or AS "image"
RENDER always returns a URL, never a file written into your namespace and never a share. Which URL depends on AS, optional and defaulting to "html":
| Form | Returns |
|---|---|
AS "html" (default) | A https://ocalt.com/maps/<token>.html URL serving a live, interactive map page — pan, zoom, and click markers. Embed it in an <iframe> or open it directly. |
AS "image" | A https://ocalt.com/maps/<token>.png URL — a rendered 800×600 PNG snapshot of the same map. A flat image, for emails, documents, or thumbnails. |
MAP POINT LAT -33.9249 LON 18.4241 LABEL "Cape Town" SET ?p
AFTER NEW ARRAY SET ?points
AFTER APPEND ?p TO ?points
AFTER MAP RENDER ?points ZOOM 13 SET ?htmlUrl
AFTER MAP RENDER ?points ZOOM 13 STYLE "satellite" AS "image" SET ?pngUrl
AFTER EMIT ?htmlUrl
AFTER EMIT ?pngUrl
/maps/ token space, entirely separate from FILE SHARE and the per-tier share limit. Render as many as you like — none of them count against the shares documented on File Manager.Tracking — A Map That Moves
A rendered map is a photograph: every coordinate is baked into the page when it is made, and the page never changes. A tracked map is the opposite. The page is a shell that asks Ocalt where things are, so a vehicle, a courier, a fleet or a delivery moves on a map somebody is already watching, without the page being regenerated.
NEW MAP TRACK NAMED "Delivery 4821"
AT -33.9249,18.4241
ZOOM 14
REFRESH 2
FOLLOW
TRAIL
SET ?track
AFTER EMIT ?track("url")
(* https://ocalt.com/maps/t-… — give this to whoever is watching *)
The URL is a normal link. Send it in an SMS, put it in an email, drop it in an <iframe>. Whoever opens it sees the map update itself for as long as they leave it open.
| Modifier | Description |
|---|---|
NAMED "x" | The page title |
AT lat,lon | Where the map opens before anything has reported in |
REFRESH n | Seconds between updates. Default 3 |
FOLLOW | Keep the viewport centred on whatever is moving |
TRAIL | Draw the path travelled behind each marker |
ZOOM, STYLE | Exactly as they work for MAP RENDER |
Moving something
MAP MOVE writes a position. It can be called from anywhere, at any time, by any later script — a phone reporting its location, a vehicle’s tracker, a warehouse system, a simulation. AS names the marker, so one map can carry a whole fleet.
(* This runs every time the phone posts its position *)
PARSE !REQUEST("body") AS JSON SET ?in
AFTER MAP MOVE "t-4f2c…" TO ?in("lat"),?in("lon")
LABEL "Thabo — 12 min away"
HEADING ?in("heading")
AS "driver"
AFTER EMIT "ok"
QUERY "vehicles" FROM "fleet" SET ?rows
AFTER FOREACH ?rows SET ?v
OPEN
MAP MOVE ?track TO ?v("lat"),?v("lon") LABEL ?v("plate") AS ?v("id")
CLOSE
AFTER MAP POSITIONS ?track SET ?where
AFTER EMIT ?where
(* Every vehicle, where it was last seen, as ordinary OcaltQL data *)
MAP MOVE again with the same AS name moves that marker; a new name adds one. MAP REMOVE ?track MARKER "driver" takes it off the map when the trip ends.Reading positions back
MAP POSITIONS ?track returns what the map is currently showing, as ordinary data — so the same positions driving the map can drive a notification, an ETA calculation, or a row in a database.
MAP POSITIONS ?track SET ?markers
AFTER FOREACH ?markers SET ?m
OPEN
MAP ROUTE FROM COORDINATES ?m("lat") & "," & ?m("lon")
TO COORDINATES "-33.9321,18.8602" SET ?eta
AFTER IF ?eta("duration_min") IS LESS THAN 5
OPEN
NOTIFICATION ?m("label") & " is nearly there"
CLOSE
CLOSE
ROUTE — Driving, Cycling, and Walking Directions
Each endpoint — FROM, TO, and an optional VIA — takes either a place name (geocoded automatically) or exact coordinates via COORDINATES "lat,lon". Coordinates skip geocoding entirely, so they are exact and unambiguous; names are convenient but can land on the wrong same-named place, so sharpen them with COUNTRY / PROVINCE or a comma-structured string. Inside a COORDINATES value the comma and colon are interchangeable — "lat,lon" or "lat:lon".
MAP ROUTE FROM "Cape Town, South Africa" TO "Johannesburg, South Africa" SET ?r
AFTER EMIT ?r("distance_km") & " km — " & ?r("duration_min") & " min"
AFTER EMIT ?r("geometry")
AFTER EMIT ?r("steps")
MAP ROUTE FROM COORDINATES "-33.9249,18.4241" TO COORDINATES "-33.9321,18.8602" SET ?r
AFTER MAP ROUTE FROM COORDINATES "-33.9249:18.4241" TO COORDINATES "-33.9321:18.8602" SET ?r2
MAP ROUTE FROM "Cape Town" TO "Stellenbosch" COUNTRY "za" PROVINCE "Western Cape" SET ?r
MAP ROUTE FROM "Cape Town" TO "Stellenbosch" PROFILE "cycling" SET ?r
AFTER MAP ROUTE FROM "Cape Town" TO "Stellenbosch" PROFILE "walking" SET ?r2
MAP ROUTE FROM "Cape Town" VIA "Bloemfontein" TO "Johannesburg" SET ?r
AFTER MAP ROUTE FROM COORDINATES "-33.9249,18.4241" VIA COORDINATES "-29.1,26.2" TO COORDINATES "-26.2,28.05" SET ?r2
MAP ROUTE FROM "Cape Town" TO "Johannesburg" ALTERNATIVES SET ?routes
AFTER EMIT ?routes(0)("distance_km") & " km"
PROFILE accepts "driving" (default), "cycling", or "walking" — MODE is an accepted alias. ALTERNATIVES makes the result an array of route objects instead of a single one. Without it, ?r is one object with distance_km, duration_min, geometry (a GeoJSON array of [lon,lat] points), and steps.
MATRIX — Many-to-Many Distances
MAP MATRIX ?points SET ?table
SNAP — Snap a Point to the Road Network
MAP SNAP LAT -33.9251 LON 18.4239 SET ?snapped
DISTANCE — Straight-Line Distance
MAP DISTANCE returns the great-circle (haversine) distance in kilometres between two endpoints. Like ROUTE, each endpoint is either a place name (geocoded, sharpened by COUNTRY / PROVINCE) or exact COORDINATES "lat,lon" — comma or colon. This is straight-line distance, not travel distance; use ROUTE for road distance.
MAP DISTANCE FROM COORDINATES "-33.9249,18.4241" TO COORDINATES "-26.2041,28.0473" SET ?km
AFTER EMIT ?km & " km"
MAP DISTANCE FROM "Cape Town, South Africa" TO "Johannesburg, South Africa" SET ?km
AFTER EMIT ?km & " km"
CLUSTER and BOUNDS
MAP CLUSTER ?places SET ?clusters
MAP BOUNDS ?places SET ?bounds
MAP LIVE — Interactive Maps the Visitor Can Draw On
Every MAP verb above produces a static result — a URL, an address, a distance. MAP LIVE is the opposite: it renders a real, interactive map into the page the visitor is looking at, lets them drop pins, draw lines, and draw polygons, and hands what they drew back to the script as ordinary OcaltQL values — the exact same shapes MAP POINT, MAP LINE, and MAP POLYGON produce, ready to feed straight into MAP RENDER or a database.
It has two forms. The halt-and-resume form (default, with SET) works exactly like GATHER and CONFIRM — it renders a centered popup, halts the script, and resumes once the visitor presses Done. The HTML node form (AS NODE) produces a native HTML node you embed anywhere in your own document at your own size, submitted through your own form.
Halt-and-Resume Form
MAP LIVE
CENTER "-33.9249,18.4241" ZOOM 13
PINS 1
LABEL "Tap your delivery location, then press Done"
SET ?result
AFTER EMIT ?result("points")(0)("lat") & ", " & ?result("points")(0)("lon")
MAP LIVE
FULLSCREEN STYLE "dark" AUTOFIT
PINS "unlimited"
DRAW LINES
DRAW POLYGONS
LABEL "Mark every stop, route, and delivery zone"
SET ?result
AFTER EMIT "Pins: " & COUNT ?result("points")
AFTER EMIT "Lines: " & COUNT ?result("lines")
AFTER EMIT "Zones: " & COUNT ?result("polygons")
?result is an object with three keys — points, lines, and polygons — each an array. Every entry is identical in shape to what the matching build verb produces, so the whole result can be handed straight to MAP RENDER.
MAP LIVE CENTER "-33.9249,18.4241" ZOOM 12 PINS "unlimited"
LABEL "Drop the pins you want on the final map"
SET ?drawn
AFTER MAP RENDER ?drawn("points") ZOOM 12 STYLE "satellite" AUTOFIT SET ?url
AFTER EMIT ?url
Pre-Placing Shapes Before the Visitor Starts
The composition slots from MAP RENDER — ?points as the residue, plus LINES, POLYGONS, and CIRCLES — carry over unchanged. Anything passed in is drawn on the map before the visitor touches it, giving them existing context to add to.
SELECT ROWS FROM DB "shop" TABLE "stores" SET ?rows
AFTER NEW ARRAY SET ?existing
AFTER FOREACH ?rows SET ?row
OPEN
MAP POINT LAT ?row("lat") LON ?row("lon") LABEL ?row("name") SET ?pt
AFTER APPEND ?pt TO ?existing
CLOSE
AFTER MAP LIVE ?existing
ZOOM 11 AUTOFIT
PINS 1
LABEL "Existing stores are shown — drop a pin where the new one goes"
SET ?result
AFTER EMIT ?result("points")(0)("lat") & ", " & ?result("points")(0)("lon")
Centering on the Visitor — with USER DEVICE
Location consent stays the job of USER DEVICE — MAP LIVE never asks for a permission itself. Grant geolocation through USER DEVICE first, then CENTER VISITOR tells the map to center on the visitor's own position using that already-granted permission. Always handle the denied case — here, by falling back to a GEOIP-derived center.
USER DEVICE GEOLOCATION LABEL "Allow location so the map opens where you are" SET ?device
AFTER IF ?device("permissions")("geolocation") IS EQUAL TO "granted"
OPEN
MAP LIVE CENTER VISITOR ZOOM 15 PINS 1
LABEL "Confirm your exact location"
SET ?result
CLOSE
OR
OPEN
GEOIP LAT SET ?lat
AFTER GEOIP LON SET ?lon
AFTER MAP LIVE CENTER (?lat & "," & ?lon) ZOOM 11 PINS 1
LABEL "We could not access your location — please drop a pin"
SET ?result
CLOSE
AFTER EMIT ?result("points")(0)("lat") & ", " & ?result("points")(0)("lon")
HTML Node Form — Embedding at Your Own Size
Instead of halting, AS NODE NAME "field" returns a native HTML node — the same kind NEW HTML NODE produces — which you append into your own document with HTML APPEND. The map becomes part of a normal form: on submit, !POST('field') carries everything the visitor drew as a JSON string, read back with PARSE ... AS JSON.
NEW HTML SET ?page
OPEN
HTML "body"
OPEN
HTML "h1" TEXT "Report a Pothole"
AFTER HTML "form" ATTR "method" WITH "POST"
OPEN
HTML "div" ATTR "id" WITH "mapslot"
AFTER HTML "button" ATTR "type" WITH "submit" TEXT "Submit Report"
CLOSE
CLOSE
CLOSE
AFTER MAP LIVE CENTER "-33.9249,18.4241" ZOOM 14
WIDTH "100%" HEIGHT 400
PINS 1
AS NODE NAME "location"
SET ?mapnode
AFTER HTML APPEND ?mapnode INTO ?page WHERE ID "mapslot"
AFTER EMIT ?page
PARSE !POST('location') AS JSON SET ?drawn
AFTER EMIT ?drawn("points")(0)("lat") & ", " & ?drawn("points")(0)("lon")
AFTER MAP RENDER ?drawn("points") ZOOM 14 SET ?url
AFTER EMIT ?url
MAP LIVE Modifier Reference
| Modifier | Description |
|---|---|
?points (residue) | Pre-placed points drawn before the visitor starts — same as MAP RENDER |
LINES ?lines / POLYGONS ?polygons / CIRCLES ?circles | Pre-placed shapes, identical to MAP RENDER |
CENTER "lat,lon" | Explicit center coordinate |
CENTER VISITOR | Center on the visitor's browser location — requires a granted USER DEVICE GEOLOCATION |
ZOOM n | Initial zoom level |
STYLE "preset" | Tile style — the same presets as MAP RENDER |
AUTOFIT | Fit the view to all pre-placed shapes |
WIDTH n|"n%" / HEIGHT n|"n%" | Map dimensions. Default 100% × 480px |
FULLSCREEN | Full-viewport map, overriding WIDTH/HEIGHT |
PINS "unlimited" | PINS n | Allow pin dropping, capped at n. Omit entirely to disable pins |
DRAW LINES | Allow the visitor to draw lines |
DRAW POLYGONS | Allow the visitor to draw polygons |
LABEL "..." | Instruction banner shown above the map, alongside the Done button |
AS NODE NAME "field" | Return an embeddable HTML node instead of halting; submits drawn data as JSON under field |
SET ?result | Halt-and-resume: captures {points, lines, polygons} once the visitor presses Done |
MAP LIVE uses the same round-trip mechanism as GATHER, CONFIRM, and USER DEVICE — it renders, halts, and resubmits to the same script once the visitor presses Done. The plain credentialed API has no browser round-trip to resubmit against, the same boundary documented on Session & Cookies. The AS NODE form has no such restriction — it only builds an HTML node, exactly like any other HTML verb.