50,000 free calls a month, card-free. Get an API key →

Documentation menu
docs / store-finder · raw .md

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:

NeedWhat serves it
Pins, clustering, click-for-details on a web mapPlacesLayer in @mapmap/maps (client-side; your data never leaves the page)
"Search my stores" on the hosted APIPUT /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 linePlacesLayer.nearest() (client) or GET /places/nearest (hosted)
Nearest store, by drive timePlacesLayer.nearestByDriveTime()POST /matrix (one origin against N stores is one call)
Stores along a route, with real detour costsPOST /route/along
Directions / turn-by-turn to a storePOST /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:

ts
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:

ts
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:

sh
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.

sh
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 /places routes and their validation rules
  • Analysis APIs: POST /matrix for drive-time ranking, POST /route/along for along-route search
  • Quickstart: issue a key in one call