Documentation menu
Store finder: bring your own places
The worked recipe for the most common custom-data ask: "we have N stores; we want a store finder on MapMap". Everything here works today with a gateway key; nothing requires a custom build. The moving parts:
| Need | What serves it |
|---|---|
| Pins, clustering, click-for-details on a web map | PlacesLayer in @mapmap/maps (client-side; your data never leaves the page) |
| "Search my stores" on the hosted API | PUT /places + GET /places/search (per-key dataset, ranked by the same engine as territory geocoding) |
| "Type a postcode, find my nearest store" | GET /geocode to resolve the postcode, then nearest() or POST /matrix |
| Nearest store, straight line | PlacesLayer.nearest() (client) or GET /places/nearest (hosted) |
| Nearest store, by drive time | PlacesLayer.nearestByDriveTime() → POST /matrix (one origin against N stores is one call) |
| Stores along a route, with real detour costs | POST /route/along |
| Directions / turn-by-turn to a store | POST /route hosted, or offline routing on-device (see SDKs) |
Your places are your data: on the web they render client-side and only touch the gateway if you opt into hosted search or matrix ranking; on the hosted API they are scoped to your key alone; in offline territory packages they ship as their own signed layer, never mixed into OSM-derived tables (the ODbL share-alike boundary, explained in licensing).
Web: pins, clusters, popups in about 15 lines
PlacesLayer (full API in the
add your own data guide and the
@mapmap/maps README) takes
an array of places or a GeoJSON FeatureCollection of Points:
import { createMap, PlacesLayer } from "@mapmap/maps";
import "maplibre-gl/dist/maplibre-gl.css";
const map = createMap({ container: "map", apiKey: "snk_…" });
const stores = new PlacesLayer(map, {
places: storesJson, // [{id, name, lat, lon, properties?}, …] or GeoJSON
cluster: true,
fitBounds: true,
popup: (p) => `<strong>${p.name}</strong><br>${p.properties?.hours ?? ""}`,
onPlaceClick: (p) => showDetailPanel(p),
});
// Nearest three by straight line (pure client-side, no network):
stores.nearest({ lat: 51.5, lon: -0.13 }, 3);
// Nearest three by real drive time (one metered /matrix call):
await stores.nearestByDriveTime({ lat: 51.5, lon: -0.13 }, { n: 3 });
Clicking a cluster zooms to expand it; setPlaces() swaps the dataset in
place. icon: { url } replaces the default circle pin, and icon also
takes a record of images keyed by a feature property (iconProperty,
default "kind"), so a depot, a shop and a locker each draw differently;
places whose image has not loaded yet fall back to a circle. label: true
labels every unclustered place with its name (an options object picks a
different property, size and colours); labels carry a halo and hide before
they collide. For anything the helper does not cover, the raw MapLibre map
is one field away (map.map): the helper is convenience, not a wall.
List-to-map sync
A store finder is usually a results list beside a map. select(id) opens
the configured popup at a place and eases the camera to it, returning the
Place (or undefined for an unknown id); deselect() closes the popup:
resultRow.addEventListener("click", () => {
stores.select("man-01", { zoom: 13 });
});
select deliberately does not fire onPlaceClick: a programmatic
selection is not a user click.
Postcode to nearest store
Resolve the postcode with the hosted geocoder, then rank:
export BASE=https://api.mapmap.ai
export API_KEY=snk_...
curl -fsS "$BASE/geocode?q=CR0+1PB&limit=1" \
-H "Authorization: Bearer $API_KEY"
The response is a GeoJSON FeatureCollection; take the first feature's
coordinates and hand them to stores.nearest(...) (free, client-side) or
stores.nearestByDriveTime(...) (one /matrix call, real road-network
answer, truck costing available).
Hosted search: "MyBrand Croydon" resolves to your store
Upload once, search from any client: the search box, the MCP tools, a
kiosk. Contracts and validation limits are in the
API reference: up to
10,000 places per key, properties at most 2 KiB serialised per place,
atomic replace-and-reindex on every upload.
curl -fsS -X PUT "$BASE/places" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d @stores.json # {"places": [{"id", "name", "lat", "lon", "locality", …}]}
curl -fsS "$BASE/places/search?q=MyBrand+Croydon&lat=51.5&lon=-0.12" \
-H "Authorization: Bearer $API_KEY"
Ranking is full-text relevance with proximity decay, served by the same
engine as the territory geocode indexes, so brand + town queries behave the
way users expect. properties (hours, phone, URL) round-trip untouched, so
the search response is directly renderable. The dataset is scoped to your
key: no other key can read or search it.
Uploading also lights up
POST /route/along:
"which of our stores is on my route, and what does the stop really cost?",
ranked by engine-measured detour time.
Browser calls and CORS
The gateway answers with Access-Control-Allow-Origin: *, so browser calls
work from ordinary web apps and from null-origin contexts alike (Electron
apps, file:// pages). Keys travel as bearer headers, never cookies, which
is what makes the open policy safe. Self-hosted deployments control this
with SN_CORS_ORIGINS.
Mobile and offline
The Android and iOS SDKs are headless navigation cores: the host app owns the MapLibre Native map view, so store pins are ordinary MapLibre annotations over a JSON asset bundled with your app. That path is fully offline and involves no MapMap service. "Navigate to this store" is first-class: offline routing to the store's lat/lon, then the guidance engine for turn-by-turn (see SDKs).
Signed offline territory packages can also carry your places as a separate signed layer with your attribution recorded in the package manifest, built by the same factory that builds the map layers. On-device search over that layer is not exposed in the mobile SDK APIs yet, so for pin display a bundled JSON asset in the host app remains the simplest path. Custom package builds are part of self-host and OEM engagements: contact us.
Next steps
- Add your own data: the wider custom-data guide (choropleths, heatmaps, isochrones, static maps)
- API reference: the five
/placesroutes and their validation rules - Analysis APIs:
POST /matrixfor drive-time ranking,POST /route/alongfor along-route search - Quickstart: issue a key in one call