Why This Helps

Mapsi has 18 APIs that combine in non-obvious ways. Without context, AI assistants default to Google Maps or Mapbox patterns β€” wrong auth headers, wrong response field names, wrong endpoint paths. These prompts fix that. Each one includes the exact Mapsi API surface for that task, so the AI generates code that actually runs.

Integration Prompts β€” framework setup
01
Migrate from Google Maps to Mapsi Integration

Swap Google Maps geocoding, places, tiles, and routing API calls with exact Mapsi equivalents. Drop-in replacement.

/v1/geocode /v1/autocomplete /v1/places /v1/route /v1/tiles/styles
β–Ό
I'm migrating my app from Google Maps Platform to Mapsi. Generate a complete migration guide and replacement code.

## My Current Stack
- Framework: [JavaScript / React / Vue / Next.js / Node.js]
- Google Maps APIs I'm using: [Geocoding API / Places Autocomplete / Directions API / Maps JavaScript API / Static Maps API]
- Language: [JavaScript / TypeScript / Python]

## Mapsi API Details
- Base URL: https://mapsi.dev
- Auth: X-API-Key header (server-side) or ?key= param (tiles only, browser-safe)
- All endpoints return JSON unless noted

## API Mapping β€” Google β†’ Mapsi

### Geocoding
- Google: GET https://maps.googleapis.com/maps/api/geocode/json?address=...&key=KEY
- Mapsi:  GET https://mapsi.dev/v1/geocode?q=Eiffel+Tower+Paris&limit=1
  Headers: X-API-Key: YOUR_KEY
  Response: { "results": [{ "coordinates": { "lat": 48.8584, "lon": 2.2945 }, "formatted_address": "..." }] }

### Reverse Geocoding
- Google: GET .../geocode/json?latlng=48.8584,2.2945&key=KEY
- Mapsi:  GET https://mapsi.dev/v1/reverse?lat=48.8584&lon=2.2945
  Response: { "results": [{ "formatted_address": "...", "city": "Paris", "country": "France" }] }

### Places Autocomplete
- Google: GET .../place/autocomplete/json?input=main+str&key=KEY
- Mapsi:  GET https://mapsi.dev/v1/autocomplete?text=main+str&limit=5
  Response: { "suggestions": [{ "text": "Main Street, London", "lat": 51.5, "lon": -0.1 }] }
  Optional: &countries=US,GB to narrow results. &focus.lat=X&focus.lon=Y to bias locally.

### Places Search
- Google: GET .../place/nearbysearch/json?location=48.85,2.29&radius=1000&keyword=restaurant&key=KEY
- Mapsi:  GET https://mapsi.dev/v1/places?q=restaurant&lat=48.8584&lon=2.2945&radius=1000&limit=10
  Response: { "places": [{ "name": "...", "lat": ..., "lon": ..., "categories": [...] }] }

### Directions / Routing
- Google: GET .../directions/json?origin=A&destination=B&key=KEY
- Mapsi:  POST https://mapsi.dev/v1/route
  Body: { "waypoints": [{"lat":48.8584,"lon":2.2945}, {"lat":48.853,"lon":2.3499}], "mode": "auto", "units": "km" }
  Response: { "distance_km": 3.2, "duration_sec": 720, "legs": [...], "polyline": { "type": "LineString", "coordinates": [...] } }
  Travel modes: "auto" (car), "truck", "bicycle", "pedestrian", "motor_scooter"

### Map Tiles (MapLibre GL replaces Maps JavaScript API)
- Google: Maps JavaScript API with google.maps.Map
- Mapsi:  MapLibre GL JS with style URL:
  new maplibregl.Map({ container: 'map', style: 'https://mapsi.dev/v1/tiles/styles?style=light&key=YOUR_KEY' })
  Available styles: light, dark, white, grayscale, black, streets, topo, liberty

### Static Maps
- Google: GET https://maps.googleapis.com/maps/api/staticmap?center=48.85,2.29&zoom=15&size=600x400&key=KEY
- Mapsi:  GET https://mapsi.dev/v1/static-map?lat=48.8584&lon=2.2945&zoom=15&width=600&height=400
  Headers: X-API-Key: YOUR_KEY  β†’ Returns PNG

## What to Generate
1. Side-by-side migration table (Google API call β†’ Mapsi equivalent)
2. Full replacement code for every Google API call in my app
3. Auth migration: remove API key from URL params, move to X-API-Key header (except tiles β€” use ?key= param for MapLibre)
4. MapLibre GL setup replacing google.maps.Map β€” same center/zoom, same markers (maplibregl.Marker), same click handlers
5. Response field remapping: where Google returns "geometry.location.lat/lng", Mapsi returns "coordinates.lat/lon"
6. Backend proxy if any Mapsi calls need to be server-side (to keep X-API-Key out of browser)

## Code Requirements
- Show before/after for every replaced call β€” makes review easy
- Flag any Google feature that has no direct Mapsi equivalent (Street View, Traffic layer) β€” suggest workaround or note as out of scope
- Preserve all existing UI behaviour β€” same markers, same popups, same map interactions

Generate the migration code now.
02
Add an Interactive Map to a React App Integration

MapLibre GL + Mapsi tiles + geocoding search bar + markers. Full React component, production patterns.

/v1/tiles/styles /v1/geocode /v1/autocomplete
β–Ό
I'm adding an interactive map to a React app using Mapsi. Generate a complete, production-ready implementation.

## App Context
- Framework: React [18+ / Next.js 14+ / Vite]
- Language: TypeScript
- Use Case: [e.g., show store locations, display delivery route, user picks a location]
- Map style preference: [light / dark / streets / topo β€” or leave as light default]

## Mapsi Map Tiles API
Style URL (pass directly to MapLibre β€” includes tiles, fonts, sprites):
  https://mapsi.dev/v1/tiles/styles?style=light&key=YOUR_API_KEY

Note: The tiles API key goes in the URL as ?key= param (safe for browser).
All other Mapsi APIs (geocode, places, etc.) must use X-API-Key header β€” server-side only.

Available styles: light, dark, white, grayscale, black, streets, topo, liberty

## Mapsi Geocoding API (for search)
GET https://mapsi.dev/v1/geocode?q=Eiffel+Tower+Paris&limit=1
Headers: X-API-Key: YOUR_KEY  ← server-side proxy only
Response: { "results": [{ "coordinates": { "lat": 48.8584, "lon": 2.2945 }, "formatted_address": "..." }] }

## Mapsi Autocomplete API (for search-as-you-type)
GET https://mapsi.dev/v1/autocomplete?text=eiffel&limit=5
Headers: X-API-Key: YOUR_KEY  ← server-side proxy only
Response: { "suggestions": [{ "text": "Eiffel Tower, Paris, France", "lat": 48.8584, "lon": 2.2945 }] }

## What to Generate
1.  React component:
   - MapLibre GL JS map (npm: maplibre-gl)
   - Style from Mapsi tiles URL
   - Accepts: center, zoom, markers (array of {lat, lon, label}), optional onClick handler
   - maplibregl.Marker for each location, with popup on click
   - map.flyTo() for smooth navigation
   - Responsive: fill container div, handle resize

2.  component:
   - Input field β†’ debounce 350ms β†’ call backend proxy β†’ show dropdown suggestions
   - On selection: place marker on map, fly to location
   - Clear button, keyboard navigation (arrow keys + Enter)

3. Next.js API route (or Express endpoint) as proxy:
   GET /api/autocomplete?text=... β†’ calls Mapsi autocomplete with X-API-Key
   GET /api/geocode?q=... β†’ calls Mapsi geocode with X-API-Key
   (Keeps API key server-side, never exposed to browser)

4. useMap() custom hook: exposes addMarker(), removeMarker(), flyTo(), getCurrentBounds()

5. TypeScript types: MapMarker, MapCenter, SearchResult interfaces

## Code Requirements
- Install instructions: npm install maplibre-gl @types/maplibre-gl
- Import maplibre-gl/dist/maplibre-gl.css in layout
- useEffect cleanup: map.remove() on unmount to prevent memory leaks
- SSR safe for Next.js: dynamic import with ssr: false, or useEffect guard
- No API key in any client-side code β€” only in .env.local on server

Generate the code now.
03
Secure Backend Proxy for Mapsi Integration

Server-side wrapper keeping X-API-Key hidden, with response caching, rate limit handling, and request validation.

X-API-Key pattern Cache layer 429 handling
β–Ό
I need a secure backend proxy for Mapsi so my API key is never exposed in browser code. Generate a complete implementation.

## Context
- Runtime: [Node.js (Express) / Python (FastAPI) / Next.js API routes / PHP Laravel]
- Language: [TypeScript / JavaScript / Python / PHP]
- Caching: [Redis / in-memory / none]

## Mapsi Auth Rules
- All non-tile endpoints require: X-API-Key: YOUR_API_KEY header (server-side only)
- Tiles endpoint ONLY: key goes in URL as ?key= param β€” this is the one exception that can be browser-direct
- Never put X-API-Key in browser JavaScript β€” it will be visible in DevTools

## Mapsi Rate Limits by Plan
- Free: 2 req/sec, 1,000/day
- Growth: 15 req/sec, 35,000/day
- Business: 25 req/sec, 140,000/day
- On 429: { "error": "Rate limit exceeded" } β€” must back off and retry

## Endpoints to Proxy (all GET unless noted)
  /api/geocode?q=...          β†’ https://mapsi.dev/v1/geocode?q=...
  /api/autocomplete?text=...  β†’ https://mapsi.dev/v1/autocomplete?text=...
  /api/reverse?lat=&lon=      β†’ https://mapsi.dev/v1/reverse?lat=&lon=
  /api/places?q=&lat=&lon=    β†’ https://mapsi.dev/v1/places?q=&lat=&lon=
  /api/route                  β†’ POST https://mapsi.dev/v1/route  (forward body)
  /api/isochrone?lat=&lon=&time= β†’ https://mapsi.dev/v1/isochrone?...
  /api/timezone?lat=&lon=     β†’ https://mapsi.dev/v1/timezone?...
  /api/pip?lat=&lon=          β†’ https://mapsi.dev/v1/pip?lat=&lon=

## What to Generate
1. Generic proxy handler: validates incoming params, forwards to Mapsi with X-API-Key header, returns response
2. Input validation: lat must be -90 to 90, lon -180 to 180, q/text must be non-empty strings, max length 200
3. Response caching:
   - Geocode / reverse / PIP / timezone: cache 24 hours (stable data)
   - Autocomplete: cache 5 minutes (slightly dynamic)
   - Route / isochrone: cache 1 hour
   - Cache key: md5(endpoint + sorted params)
4. Rate limit handling: on 429 from Mapsi, return 429 to client with Retry-After header, log incident
5. Error normalisation: map Mapsi 400/401/404 to appropriate client-facing messages (don't leak internal errors)
6. Request logging: log endpoint, params (not full addresses for privacy), response_ms, cache hit/miss
7. CORS: allow your frontend origin only
8. .env.example with MAPSI_API_KEY, REDIS_URL, ALLOWED_ORIGIN

## Code Requirements
- Single proxy route handler β€” parametric path so adding new endpoints doesn't require new code
- Never log full coordinates or addresses in production (privacy)
- Return Mapsi response_ms in a X-Response-Time header for monitoring

Generate the code now.
Use Case Prompts β€” multi-API combinations
04
Store Locator on a Website Use Case

Autocomplete search β†’ geocode β†’ nearest places β†’ tile map with pins + static map thumbnails per result.

/v1/autocomplete /v1/geocode /v1/places /v1/tiles/styles /v1/static-map
β–Ό
I'm building a store locator for my website using Mapsi. Generate a complete implementation.

## Context
- Framework: [React / Vue / Vanilla JS / Next.js]
- Backend: [Node.js / Python / PHP]
- Use Case: User types their address or city, sees nearest store locations on a map and in a list

## Mapsi APIs Used (5 API calls total)

### 1. Autocomplete (as user types)
GET https://mapsi.dev/v1/autocomplete?text=high+str&limit=5
Headers: X-API-Key: YOUR_KEY  (server-side proxy)
Response: { "suggestions": [{ "text": "High Street, London", "lat": 51.509, "lon": -0.118 }] }

### 2. Geocode (when user selects a suggestion or submits)
GET https://mapsi.dev/v1/geocode?q=High+Street+London&limit=1
Response: { "results": [{ "coordinates": { "lat": 51.509, "lon": -0.118 }, "formatted_address": "..." }] }
Note: Use the lat/lon from the autocomplete suggestion directly if available β€” saves an extra geocode call.

### 3. Places search (find stores near geocoded point)
GET https://mapsi.dev/v1/places?q=[YOUR_STORE_CATEGORY]&lat=51.509&lon=-0.118&radius=10000&limit=10
Response: { "places": [{ "name": "...", "lat": ..., "lon": ..., "address": "..." }] }
Note: Replace [YOUR_STORE_CATEGORY] with your category keyword e.g. "pharmacy", "coffee", "electronics"
OR: Use your own store database β€” query by distance from geocoded lat/lon instead of calling /places.

### 4. Map tiles (interactive map with pins)
MapLibre style URL: https://mapsi.dev/v1/tiles/styles?style=light&key=YOUR_API_KEY
(?key= in URL is safe for browser β€” tiles-only exception)
Add a maplibregl.Marker for each store result.

### 5. Static map (thumbnail per result in list view)
GET https://mapsi.dev/v1/static-map?lat=51.509&lon=-0.118&zoom=14&width=200&height=120
Headers: X-API-Key: YOUR_KEY (server-side) OR use as img src with key in URL for browser convenience
Returns: PNG image

## What to Generate
1. Search bar component: autocomplete dropdown with debounce (350ms), keyboard nav, "use my location" button (browser geolocation)
2. On search submit: geocode address β†’ find stores nearby β†’ render results
3. Results list: store name, address, distance from search point, static map thumbnail (200Γ—120)
4. Interactive MapLibre map: all stores as markers, click marker β†’ highlight list item β†’ show popup with name + address
5. "Get directions" link per result: opens native maps (Google Maps / Apple Maps) with destination pre-filled
6. Backend routes (through your server proxy): /api/autocomplete, /api/geocode, /api/places, /api/static-map
7. Distance calculation: Haversine distance from user's search point to each store β€” display as "2.3 km away"
8. Empty state: "No stores found within 10km β€” showing nearest 3" fallback with expanded radius

## Code Requirements
- Never call Mapsi directly from browser except for tile styles URL
- Lazy-load MapLibre GL to not block initial page render
- Accessible: keyboard-navigable results list, aria-labels on map markers
- Mobile: list view default on small screens, map toggle button

Generate the code now.
05
Address Autocomplete in a Form or Checkout Use Case

Live suggestions β†’ user selects β†’ normalize β†’ reverse confirm β†’ structured fields populated. Reduces address errors at checkout.

/v1/autocomplete /v1/normalize /v1/reverse
β–Ό
I'm building an address autocomplete input for a checkout or form using Mapsi. Generate a complete implementation.

## Context
- Framework: [React / Vue / Vanilla JS]
- Backend: [Node.js / Python]
- Goal: Reduce address input errors β€” suggest valid addresses as user types, then parse into structured fields

## Mapsi APIs Used (3 API calls total)

### 1. Autocomplete (real-time suggestions)
GET https://mapsi.dev/v1/autocomplete?text=10+downing&limit=5
Headers: X-API-Key: YOUR_KEY  (server-side proxy β€” never in browser)
Optional: &countries=GB to restrict. &focus.lat=X&focus.lon=Y to bias toward user's region.
&layers=address to return address-level results only (filters out city/country suggestions)
Response: { "suggestions": [{ "text": "10 Downing Street, London, SW1A 2AA", "lat": 51.503, "lon": -0.127 }] }

### 2. Normalize (parse selected address into components)
POST https://mapsi.dev/v1/normalize
Headers: X-API-Key: YOUR_KEY, Content-Type: application/json
Body: { "address": "10 Downing Street, London, SW1A 2AA" }
Response: {
  "parsed": {
    "house_number": "10",
    "road": "Downing Street",
    "city": "London",
    "postcode": "SW1A 2AA",
    "country": "United Kingdom"
  },
  "normalized": ["10 downing street london sw1a 2aa"]
}
Use case: auto-populate house_number, street, city, postcode form fields after user selects a suggestion.

### 3. Reverse geocode (confirm coordinates β†’ address, optional quality check)
GET https://mapsi.dev/v1/reverse?lat=51.503&lon=-0.127
Response: { "results": [{ "formatted_address": "10 Downing St, London SW1A 2AA, UK" }] }
Use case: if user enables browser geolocation, auto-fill form with their current address.

## What to Generate
1. AddressAutocomplete component:
   - Single text input field
   - Debounce 300ms before calling autocomplete
   - Dropdown showing up to 5 suggestions (text from API)
   - Keyboard navigation: arrow keys, Enter to select, Escape to close
   - On selection: call normalize β†’ populate street, city, postcode, country fields automatically
   - Show spinner during API calls, error state if API fails (fall back to plain text input)

2. "Use my location" button:
   - navigator.geolocation.getCurrentPosition β†’ call reverse geocode β†’ populate same fields
   - Handle permission denied gracefully

3. Backend proxy routes (server-side):
   GET /api/autocomplete?text=...&countries=...
   POST /api/normalize  (forward body)
   GET /api/reverse?lat=...&lon=...

4. Validation after population:
   - Warn if postcode is empty (normalize may not always return it)
   - Warn if country doesn't match expected shipping countries

## Code Requirements
- Minimum 2 characters before triggering autocomplete
- Debounce on input, not on API call β€” cancel in-flight requests if user keeps typing
- Accessible: role="combobox", aria-autocomplete="list", aria-expanded, proper keyboard handling
- Never call Mapsi directly from browser β€” all calls through your backend proxy
- Handle Mapsi 429 gracefully: show "Too many requests β€” please type your address manually"

Generate the code now.
06
Route a Delivery Use Case

Geocode pickup + drop-off β†’ compute route β†’ get distance and ETA β†’ draw polyline on MapLibre tile map.

/v1/geocode /v1/route /v1/tiles/styles
β–Ό
I'm building a delivery routing feature using Mapsi. Generate a complete implementation.

## Context
- Framework: [React / Vanilla JS / Node.js backend]
- Use Case: Dispatcher enters pickup and drop-off addresses, sees the route on a map with distance and ETA

## Mapsi APIs Used (3 API calls β€” matches the Route a Delivery demo)

### 1. Geocode pickup address
GET https://mapsi.dev/v1/geocode?q=10+Downing+Street+London&limit=1
Headers: X-API-Key: YOUR_KEY
Response: { "results": [{ "coordinates": { "lat": 51.503, "lon": -0.127 }, "formatted_address": "..." }] }

### 2. Route API (compute route between geocoded coordinates)
POST https://mapsi.dev/v1/route
Headers: X-API-Key: YOUR_KEY, Content-Type: application/json
Body:
{
  "waypoints": [
    { "lat": 51.503, "lon": -0.127 },   // pickup
    { "lat": 51.509, "lon": -0.118 }    // drop-off
  ],
  "mode": "auto",   // auto (car), truck, bicycle, pedestrian, motor_scooter
  "units": "km"
}
Response:
{
  "distance_km": 2.3,
  "duration_sec": 480,
  "waypoint_count": 2,
  "legs": [{ "from": 0, "to": 1, "distance_km": 2.3, "duration_sec": 480 }],
  "polyline": { "type": "LineString", "coordinates": [[lon1,lat1], [lon2,lat2], ...] },
  "mode": "auto",
  "units": "km"
}
Note: polyline is a GeoJSON LineString β€” add directly as a MapLibre geojson source.

### 3. Map Tiles (render route on map)
MapLibre style URL: https://mapsi.dev/v1/tiles/styles?style=streets&key=YOUR_API_KEY
(?key= in URL is browser-safe for tiles only)
Add route polyline as a GeoJSON layer. Add markers for pickup and drop-off.

## What to Generate
1. Route form: two address inputs with autocomplete (use /v1/autocomplete), travel mode selector, "Calculate Route" button
2. On submit: geocode both addresses β†’ POST /v1/route β†’ display results
3. Results display:
   - Distance: "2.3 km"
   - ETA: format duration_sec as "8 min" or "1h 23m"
   - Per-leg breakdown if multi-stop (show from/to names, distance, time)
4. Map rendering (MapLibre):
   - Draw route polyline: { type: 'line', paint: { 'line-color': '#1a56db', 'line-width': 4 } }
   - Pickup marker (green), drop-off marker (red)
   - map.fitBounds() to show full route with padding
5. Multi-stop extension: allow adding intermediate stops (drag to reorder, max 25 waypoints)
6. Share link: encode waypoints in URL so route can be shared/bookmarked
7. Backend: geocode proxy (/api/geocode), route proxy (/api/route) β€” keep X-API-Key server-side

## Code Requirements
- GeoJSON coordinates from Mapsi are [lon, lat] order (GeoJSON standard) β€” MapLibre expects same
- Geocode both addresses in parallel (Promise.all) to halve latency
- Handle "no route found" (Mapsi returns 400 if coordinates are unreachable by chosen mode)
- Mode "truck" adds routing restrictions (height, weight limits) β€” useful for fleet apps

Generate the code now.
07
Reverse Locate a GPS Ping Use Case

Convert raw coordinates from a driver app or IoT webhook into a human-readable address with timezone context. Pin drops on live map.

/v1/reverse /v1/timezone /v1/tiles/styles
β–Ό
I'm building a GPS ping locator using Mapsi to convert raw coordinates into human-readable addresses. Generate a complete implementation.

## Context
- Source: [Driver mobile app / IoT device webhook / Fleet tracking system]
- Framework: [React / Node.js / Python]
- Use Case: Show where a device is right now β€” address, timezone, on a live map

## Mapsi APIs Used (matches the Reverse Locate a GPS Ping demo)

### 1. Reverse Geocoding
GET https://mapsi.dev/v1/reverse?lat=48.8584&lon=2.2945
Headers: X-API-Key: YOUR_KEY
Response:
{
  "results": [{
    "formatted_address": "Champ de Mars, 5 Av. Anatole France, 75007 Paris, France",
    "city": "Paris",
    "state": "Île-de-France",
    "country": "France",
    "postcode": "75007"
  }]
}

### 2. Timezone (for accurate local time display)
GET https://mapsi.dev/v1/timezone?lat=48.8584&lon=2.2945
Headers: X-API-Key: YOUR_KEY
Response:
{
  "timezone": "Europe/Paris",
  "utc_offset": "+01:00",
  "dst": true,
  "current_time": "Thursday, May 8, 2025 at 3:14:00 PM GMT+1"
}

### 3. Map Tiles (pin on live map)
MapLibre style URL: https://mapsi.dev/v1/tiles/styles?style=light&key=YOUR_API_KEY
Add maplibregl.Marker at the coordinate, popup with formatted_address.

## What to Generate
1. Webhook receiver (if GPS pings arrive via HTTP webhook):
   POST /webhook/gps-ping
   Body: { device_id, lat, lon, timestamp }
   Calls reverse geocode + timezone in parallel (Promise.all)
   Stores enriched record: { device_id, lat, lon, formatted_address, city, country, timezone, local_time, timestamp }

2. Live location UI:
   - MapLibre tile map with pin at current device coordinate
   - Popup on pin: formatted address + local time
   - Sidebar: device_id, coordinates, address, timezone, last seen timestamp
   - "Locate" button: enter lat/lon manually, or paste raw coordinate string (parse "48.8584, 2.2945")

3. History view: last N pings for a device as a line on the map (array of coordinates β†’ GeoJSON LineString)

4. Batch enrichment for historical pings:
   POST https://mapsi.dev/v1/batch/reverse
   Body: { "points": [{ "lat": 48.8584, "lon": 2.2945, "id": "ping-001" }, ...] }
   Response: { "results": [{ "id": "ping-001", "address": "...", "lat": ..., "lon": ... }] }
   Batch limits: Free=10, Growth=250, Business=500 points per request.
   Credits: 1 credit per 2 points (rounded up).

## Code Requirements
- Call reverse geocode + timezone in parallel, not sequentially β€” saves ~100ms per ping
- Cache reverse geocode results for coordinates within 100m of each other (grid snap) for 1 hour
- Handle coordinates over water or unpopulated areas gracefully (reverse may return null formatted_address β€” show coordinates only)
- For batch: chunk large arrays into plan-limit-sized batches, process sequentially with 200ms delay between batches

Generate the code now.
08
Multi-Stop Route Optimizer Use Case

Geocode all stops β†’ matrix for travel times β†’ optimal ordering β†’ render route on tile map. For delivery fleets.

/v1/geocode /v1/matrix /v1/route /v1/tiles/styles
β–Ό
I'm building a multi-stop route optimizer using Mapsi. Generate a complete implementation.

## Context
- Framework: [React / Node.js / Python]
- Use Case: Enter multiple delivery addresses, get the optimal visiting order, see the route on a map

## Mapsi APIs Used

### 1. Geocode all stops (batch geocode or parallel single geocodes)
GET https://mapsi.dev/v1/geocode?q=ADDRESS&limit=1
Headers: X-API-Key: YOUR_KEY
Or for large lists: POST https://mapsi.dev/v1/batch/geocode
Body: { "addresses": ["address 1", "address 2", ...], "limit": 1 }
Response: array of { "results": [{ "coordinates": { "lat": ..., "lon": ... }, "formatted_address": "..." }] }

### 2. Matrix API (travel times between all pairs β€” needed for optimisation)
POST https://mapsi.dev/v1/matrix
Headers: X-API-Key: YOUR_KEY, Content-Type: application/json
Body:
{
  "sources": [{"lat": 48.85, "lon": 2.29}, ...],  // all stops including depot
  "targets": [{"lat": 48.85, "lon": 2.29}, ...],  // same list
  "profile": "car",   // car, truck, bicycle, pedestrian
  "units": "metric"
}
Response:
{
  "durations": [[0, 720, 480], [720, 0, 360], [480, 360, 0]],  // seconds[i][j]
  "distances": [[0, 2300, 1800], ...],                          // metres[i][j]
  "durations_text": [["0s", "12m", "8m"], ...]
}
Matrix limits: Free=5Γ—5 (25 pairs), Growth=10Γ—10 (100), Business=25Γ—25 (625)

### 3. Route API (render optimised sequence as polyline)
POST https://mapsi.dev/v1/route
Body: { "waypoints": [ordered stops as {lat,lon}], "mode": "auto", "units": "km" }
Response: { "distance_km": ..., "duration_sec": ..., "polyline": { GeoJSON LineString } }

### 4. Map Tiles
MapLibre: https://mapsi.dev/v1/tiles/styles?style=streets&key=YOUR_API_KEY

## What to Generate
1. Stop input UI: add/remove addresses, drag to manually reorder, max 25 stops (Mapsi route limit)
2. Geocode all addresses (parallel for ≀10, batch API for >10)
3. Matrix call: compute NΓ—N travel time matrix for all stops
4. Optimisation: implement nearest-neighbour TSP heuristic using the duration matrix:
   - Start from depot (first stop)
   - Greedily visit nearest unvisited stop
   - Return to depot
   - This gives a good-enough route for ≀15 stops without complex solver
5. Route call: POST /v1/route with optimised stop order β†’ get polyline
6. Map: draw polyline, numbered markers for each stop in visiting order
7. Summary: total distance, total time, stop-by-stop breakdown from legs[]
8. Export: "Copy optimised addresses" button, optional CSV download

## Code Requirements
- Run geocoding for all stops in parallel (Promise.all) before matrix call
- Matrix is symmetric for car profile β€” only need upper triangle, but Mapsi returns full matrix (use as-is)
- Handle geocode failures per stop (show error on that stop, proceed with others)
- For >25 stops: warn user, suggest splitting into multiple routes
- Nearest-neighbour is O(nΒ²) β€” fine for n≀25 in browser, runs in <10ms

Generate the code now.
09
Verify Agent Visit / Proof of Location Use Case

Reverse geocode a GPS ping β†’ admin PIP hierarchy β†’ nearest road β†’ timestamp with timezone. Produces a tamper-evident visit record.

/v1/reverse /v1/pip /v1/nearest-road /v1/timezone
β–Ό
I'm building a proof-of-location / agent visit verification system using Mapsi. Generate a complete implementation.

## Context
- Use Case: Field agent captures a GPS coordinate on their device. System produces a verified location record β€” address, admin area, nearest road, local time β€” stored for audit.
- Backend: [Node.js / Python]
- Mobile: [React Native / Flutter / or just a browser app with geolocation]

## Mapsi APIs Used (4 API calls β€” all in parallel)

### 1. Reverse Geocoding (human-readable address)
GET https://mapsi.dev/v1/reverse?lat=48.8584&lon=2.2945
Response: { "results": [{ "formatted_address": "...", "city": "Paris", "country": "France", "postcode": "75007" }] }

### 2. Point-in-Polygon (administrative hierarchy)
GET https://mapsi.dev/v1/pip?lat=48.8584&lon=2.2945
Response:
{
  "country": "France",
  "country_code": "FRA",
  "state": "Île-de-France",
  "city": "Paris",
  "neighbourhood": "Gros Caillou",
  "hierarchy": { "continent": "...", "country": "...", "region": "...", "city": "...", "neighbourhood": "..." }
}
Use case: unambiguous admin-level location proof (not just an address string β€” structured and queryable).

### 3. Nearest Road (road-level verification)
GET https://mapsi.dev/v1/nearest-road?lat=48.8584&lon=2.2945&limit=1
Response:
{
  "results": [{
    "road_name": "Avenue Anatole France",
    "highway": "secondary",
    "speed_limit_kmh": 50,
    "snapped_point": { "lat": 48.8582, "lon": 2.2948 },
    "distance_m": 18,
    "osm_way_id": "12345678"
  }]
}
Use case: confirms agent was near a real road (distance_m > 500 = suspicious β€” agent indoors or GPS drift).

### 4. Timezone (authoritative local time)
GET https://mapsi.dev/v1/timezone?lat=48.8584&lon=2.2945
Response: { "timezone": "Europe/Paris", "utc_offset": "+01:00", "dst": true, "current_time": "..." }
Use case: prevents timestamp disputes (device clock may be wrong; timezone from coords is authoritative).

## What to Generate
1. captureVisit(agentId, lat, lon) function:
   - Run all 4 Mapsi calls in parallel (Promise.all)
   - Assemble a VisitRecord: {
       agent_id, lat, lon, accuracy_m,
       formatted_address, city, country, postcode,  // from reverse
       admin_hierarchy,                              // from PIP
       nearest_road, road_distance_m, osm_way_id,  // from nearest-road
       timezone, local_time, utc_offset,            // from timezone
       captured_at_utc,                             // server UTC timestamp
       confidence_flags                              // derived (see below)
     }
2. Confidence flags (derived from API responses):
   - road_too_far: true if nearest road distance_m > 300 (GPS drift or indoors)
   - address_mismatch: true if reverse city β‰  PIP city (data quality warning)
   - dst_active: from timezone.dst (useful for time-sensitive workflows)
3. Visit record storage: POST /api/visits β†’ store in DB, return record ID
4. Verification UI: show map with pin, address, admin hierarchy table, road name, local time, confidence badge
5. PDF/export endpoint: GET /api/visits/:id/export β†’ formatted visit certificate (agent name, location, timestamp, map static image via /v1/static-map)
6. Mobile capture flow: browser geolocation β†’ send lat/lon/accuracy to backend β†’ get VisitRecord back β†’ display

## Code Requirements
- All 4 API calls MUST run in parallel β€” sequential would take 400ms+ per visit
- Store raw coordinates + all API responses in DB (not just derived fields) for future reprocessing
- Never accept coordinates from client as authoritative β€” compare with claimed address if provided
- road_distance_m in the hundreds of metres is normal in dense cities β€” tune threshold for your use case

Generate the code now.
10
Service Area Coverage Map (Isochrone) Use Case

Generate drive-time reachability polygons from a depot β†’ overlay on tile map β†’ show customers inside/outside coverage.

/v1/geocode /v1/isochrone /v1/tiles/styles
β–Ό
I'm building a service area coverage map using Mapsi's Isochrone API. Generate a complete implementation.

## Context
- Framework: [React / Vanilla JS / Next.js]
- Use Case: Visualise which areas are reachable from a depot within 15, 30, 60 minutes. Show coverage zone for delivery, ambulance, or service radius.

## Mapsi APIs Used

### 1. Geocode depot address
GET https://mapsi.dev/v1/geocode?q=DEPOT_ADDRESS&limit=1
Response: { "results": [{ "coordinates": { "lat": 48.85, "lon": 2.29 }, "formatted_address": "..." }] }

### 2. Isochrone API (reachability polygons)
GET https://mapsi.dev/v1/isochrone?lat=48.85&lon=2.29&time=15,30,60&profile=car
Headers: X-API-Key: YOUR_KEY
Parameters:
  - time: comma-separated minutes. Max 5 contours. Example: "10,20,30"
  - OR distance_km: comma-separated distances (cannot combine with time)
  - profile: car (default), bicycle, pedestrian
  - denoise: 0.0–1.0 polygon smoothing (default 0.5)
Response: GeoJSON FeatureCollection β€” one Polygon Feature per contour.
  Each Feature has properties.contour_minutes (or contour_km).
  Polygons are nested β€” larger contours enclose smaller ones.

### 3. Map Tiles
MapLibre: https://mapsi.dev/v1/tiles/styles?style=light&key=YOUR_API_KEY

## What to Generate
1. Depot input: address field with autocomplete β†’ geocode β†’ compute isochrone
2. Controls: travel time selector (preset buttons: 15m / 30m / 60m, or custom slider), travel mode (car/bicycle/pedestrian)
3. Render isochrone polygons on MapLibre:
   - Add as GeoJSON source: map.addSource('isochrone', { type: 'geojson', data: geojson })
   - 3 fill layers, one per contour, styled with decreasing opacity (60m=0.15, 30m=0.25, 15m=0.35)
   - Colours: innermost = strongest, outermost = lightest (e.g., blue scale)
   - Outline layer: 'line' type, 1px stroke per polygon boundary
4. Legend: colour-coded travel time key ("Within 15 min", "Within 30 min", "Within 60 min")
5. map.fitBounds() to outer isochrone polygon on load
6. Optional: customer pins β€” show which customers fall inside/outside coverage:
   - For each customer lat/lon: check if inside outermost polygon (use turf.js booleanPointInPolygon)
   - Green pin = within coverage, grey pin = outside
7. "Export Coverage" button: download the GeoJSON FeatureCollection for use in other tools

## Code Requirements
- GeoJSON from Mapsi is ready to pass to map.addSource() directly β€” no transformation needed
- Isochrone can take 1-3 seconds for complex polygons β€” show loading spinner on map
- Recompute isochrone when user changes travel mode or time (debounce 500ms)
- Backend proxy: GET /api/isochrone?lat=...&lon=...&time=... (X-API-Key server-side)

Generate the code now.
Data Pipeline Prompts β€” backend & ETL
11
Bulk Address Cleansing Pipeline Data Pipeline

Normalize messy addresses β†’ batch geocode β†’ output enriched CSV with coordinates, structured components, and match confidence.

POST /v1/normalize POST /v1/batch/geocode
β–Ό
I'm building a bulk address cleansing and geocoding pipeline using Mapsi. Generate a complete implementation.

## Context
- Language: [Python / Node.js]
- Input: CSV file with a column of raw address strings (potentially messy, abbreviated, inconsistent)
- Output: Enriched CSV with structured components + coordinates per address
- Scale: [hundreds / thousands / tens of thousands] of addresses

## Mapsi APIs Used

### 1. Address Normalisation (pre-processing before geocoding)
POST https://mapsi.dev/v1/normalize
Headers: X-API-Key: YOUR_KEY, Content-Type: application/json
Body: { "address": "123 main st new york ny 10001" }
Response:
{
  "input": "123 main st new york ny 10001",
  "parsed": {
    "house_number": "123",
    "road": "main st",
    "city": "new york",
    "state": "ny",
    "postcode": "10001"
  },
  "normalized": ["123 main street new york ny 10001", "123 main st new york new york 10001"]
}
Why normalize first: Libpostal expands abbreviations (St→Street), fixes casing, handles international formats.
Use normalized[0] as input to batch geocode for higher match rates.

### 2. Batch Geocoding
POST https://mapsi.dev/v1/batch/geocode
Headers: X-API-Key: YOUR_KEY, Content-Type: application/json
Body: { "addresses": ["123 Main Street New York NY 10001", ...], "limit": 1 }
Response: array of geocode results, one per address input.
Each result: { "results": [{ "coordinates": { "lat": ..., "lon": ... }, "formatted_address": "..." }] } OR { "results": [] } on no match.

Batch size limits by plan:
  Free: 10 per batch Β· Growth: 5,000 per batch Β· Business: 30,000 per batch

## What to Generate
1. CSV reader: parse input file, extract address column (configurable column name/index)
2. Normalize phase:
   - Call POST /v1/normalize for each address individually (no batch endpoint β€” run with concurrency limit of 5)
   - Store normalized[0] as the geocoding input. If normalized is empty, use original address.
   - Track: original_address, normalized_address, parsed components
3. Batch geocode phase:
   - Chunk normalized addresses into plan-appropriate batch sizes (configurable: default 1,000)
   - POST each chunk to /v1/batch/geocode
   - Add 200ms delay between batches to respect rate limits (15 req/sec on Growth)
   - Retry failed batches once on 5xx with exponential backoff
4. Output CSV columns:
   original_address | normalized_address | house_number | road | city | state | postcode | country |
   lat | lon | formatted_address | match_status | geocode_confidence
   match_status: "matched" | "no_match" | "error"
5. Quality report (printed to stdout after run):
   - Total processed, matched count, match rate %, no_match count
   - List of top 20 unmatched addresses for manual review
6. Resumable: checkpoint every 1,000 records to a temp file β€” if run is interrupted, skip already-processed rows

## Code Requirements
- Normalize and geocode are separate phases β€” run normalize for all rows first, then geocode
- Concurrency for normalize: max 5 parallel requests (rate limit headroom)
- Never geocode without normalizing first β€” it meaningfully improves match rates on messy data
- Log progress: "Processed 500/10000 β€” match rate so far: 94.2%"

Generate the code now.
12
GPS Telemetry Enrichment Pipeline Data Pipeline

Take raw device pings β†’ batch reverse geocode β†’ map match to road network β†’ add elevation and timezone β†’ output enriched records.

POST /v1/batch/reverse POST /v1/match POST /v1/elevation GET /v1/timezone
β–Ό
I'm building a GPS telemetry enrichment pipeline using Mapsi. Generate a complete implementation.

## Context
- Language: [Python / Node.js]
- Input: Raw GPS pings from IoT devices or fleet vehicles (device_id, lat, lon, timestamp, accuracy_m)
- Output: Enriched records with human-readable address, road info, elevation, timezone, map-matched coordinates
- Volume: [hundreds to tens of thousands of pings per batch]

## Mapsi APIs Used (4 APIs)

### 1. Batch Reverse Geocoding (address per coordinate)
POST https://mapsi.dev/v1/batch/reverse
Headers: X-API-Key: YOUR_KEY, Content-Type: application/json
Body: {
  "points": [
    { "lat": 48.8584, "lon": 2.2945, "id": "ping-001" },
    { "lat": 51.5074, "lon": -0.1278, "id": "ping-002" }
  ]
}
Response: { "results": [{ "id": "ping-001", "address": "...", "lat": 48.8584, "lon": 2.2945 }] }
Credits: 1 credit per 2 points (rounded up)
Batch limits: Free=10, Growth=250, Business=500 points per request

### 2. Map Matching (snap noisy GPS to road network)
POST https://mapsi.dev/v1/match
Headers: X-API-Key: YOUR_KEY, Content-Type: application/json
Body: {
  "points": [
    { "lat": 48.8584, "lon": 2.2945, "timestamp": 1741000000 },
    { "lat": 48.8591, "lon": 2.2962, "timestamp": 1741000015 }
  ],
  "profile": "car"   // car, bicycle, pedestrian
}
Response: {
  "confidence": 0.92,
  "geometry": { "type": "LineString", "coordinates": [...] },  // snapped road-aligned route
  "matched_points": [{ "lat": ..., "lon": ..., "distance_from_original_m": 8 }],
  "matched_roads": [{ "road_name": "Avenue Anatole France", "highway": "secondary" }],
  "distance_meters": 412
}
Max points per request: 500. Use for a contiguous GPS trace from one device trip.

### 3. Elevation (height above sea level)
POST https://mapsi.dev/v1/elevation
Headers: X-API-Key: YOUR_KEY, Content-Type: application/json
Body: { "points": [{ "lat": 48.8584, "lon": 2.2945 }, ...] }
Response: { "results": [{ "lat": ..., "lon": ..., "elevation_m": 33, "source": "srtm30" }] }
Batch limits: Free=10, Growth=128, Business=512 points

### 4. Timezone (one lookup per unique timezone region, not per ping)
GET https://mapsi.dev/v1/timezone?lat=48.8584&lon=2.2945
Response: { "timezone": "Europe/Paris", "utc_offset": "+01:00", "dst": true, "current_time": "..." }
Optimisation: cache timezone by H3 cell (resolution 4 β‰ˆ 1,000 kmΒ²) β€” timezone rarely changes within a region.

## What to Generate
1. Ingestion: read pings from [CSV / JSON / database / Kafka topic], group by device_id and time window (a "trip")
2. Phase 1 β€” Batch reverse geocode: chunk all pings into batch-size groups, call /v1/batch/reverse, map results back by id
3. Phase 2 β€” Map matching: per trip (contiguous pings from one device), call /v1/match. Chunk trips >500 pings into segments.
   Attach matched_roads and confidence to each trip record.
   Flag pings where distance_from_original_m > 50 as "high GPS drift"
4. Phase 3 β€” Elevation: batch POST /v1/elevation for all ping coordinates, attach elevation_m per ping
5. Phase 4 β€” Timezone: deduplicate coordinates to unique H3 cells (resolution 4), lookup timezone once per cell, assign to all pings in cell
6. Output schema per enriched ping:
   { device_id, original_lat, original_lon, matched_lat, matched_lon, gps_drift_m,
     address, road_name, highway_type, elevation_m, timezone, utc_offset,
     timestamp_utc, timestamp_local, trip_id, match_confidence }
7. Quality metrics: pings processed, match rate, avg GPS drift, high-drift count, unmatched address count

## Code Requirements
- All 4 phases run sequentially per batch β€” don't interleave (easier to debug, retry individual phases)
- Checkpoint after each phase to allow resuming failed runs
- H3 timezone cache prevents calling timezone API for every ping β€” huge credit savings on large datasets
- Map matching works on a trip trace, not individual pings β€” group by device + trip_id before calling /v1/match

Generate the code now.
Mapsi + Fencemaker β€” the complete logistics stack
13
Delivery Zone Check at Checkout Mapsi + Fencemaker ⬑ Fencemaker

Geocode depot β†’ Mapsi isochrone for drive-time zone β†’ push to Fencemaker as territory β†’ check any customer address against it. The complete logistics stack in one integration β€” exactly as shown in the zone-check demo.

Mapsi /v1/geocode Mapsi /v1/isochrone Mapsi /v1/tiles/styles Fencemaker POST /territories Fencemaker GET /pip
β–Ό
I'm building a delivery zone check at checkout using Mapsi and Fencemaker together. Generate a complete implementation.

## Context
- Framework: [React / Next.js / Node.js]
- Use Case: Compute a drive-time delivery zone from our depot with Mapsi, register it as a territory in Fencemaker, then check whether a customer address at checkout falls inside it.
- This matches the Mapsi + Fencemaker zone-check demo: 5 API calls total.

## API Keys
- Mapsi key: X-API-Key header on all Mapsi calls (server-side)
- Fencemaker key: X-API-Key header on all Fencemaker calls (separate key, server-side)
- Tile URLs only: use ?key= param in URL (browser-safe, Mapsi tiles only)

## Step 1 β€” Geocode depot address (Mapsi)
GET https://mapsi.dev/v1/geocode?q=DEPOT_ADDRESS&limit=1
Headers: X-API-Key: MAPSI_KEY
Response: { "results": [{ "coordinates": { "lat": 48.85, "lon": 2.29 }, "formatted_address": "..." }] }

## Step 2 β€” Generate delivery zone polygon (Mapsi Isochrone)
GET https://mapsi.dev/v1/isochrone?lat=48.85&lon=2.29&time=30&profile=car
Headers: X-API-Key: MAPSI_KEY
Response: GeoJSON FeatureCollection β€” one Polygon for the 30-minute drive-time zone
  { "type": "FeatureCollection", "features": [{ "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [[...]] } }] }

## Step 3 β€” Register polygon as a Fencemaker territory
POST https://fencemaker.app/api/v1/territories
Headers: X-API-Key: FENCEMAKER_KEY, Content-Type: application/json
Body:
{
  "name": "30-Minute Delivery Zone",
  "boundary": <GeoJSON Polygon geometry from Step 2>,
  "code": "DEPOT-30MIN"
}
Response:
{
  "id": "uuid",
  "name": "30-Minute Delivery Zone",
  "code": "DEPOT-30MIN",
  "color": "#ff5500",
  "location_id": "uuid",
  "created_at": "2026-05-06T10:00:00Z"
}
Note: Do this once on setup (or when depot or drive time changes) β€” not on every checkout.
Store the returned "id" β€” you'll need it to update the territory boundary later.

## Step 4 β€” Check customer address at checkout (Fencemaker PIP)

### Option A β€” By coordinates (fastest, use when you already have lat/lon)
GET https://fencemaker.app/api/v1/pip?lat=CUSTOMER_LAT&lon=CUSTOMER_LON
Headers: X-API-Key: FENCEMAKER_KEY
Response (matched):
{
  "matched": true,
  "territory": {
    "id": "uuid",
    "name": "30-Minute Delivery Zone",
    "code": "DEPOT-30MIN",
    "agent_id": null,
    "location_id": "uuid",
    "location_name": "Depot"
  },
  "lat": CUSTOMER_LAT, "lon": CUSTOMER_LON,
  "response_ms": 8
}
Response (no match):
{
  "matched": false,
  "territory": null,
  "lat": CUSTOMER_LAT, "lon": CUSTOMER_LON,
  "response_ms": 6
}
Use territory.code to map to your delivery config. territory.id for audit trail.

### Option B β€” By address string (Fencemaker geocodes internally)
POST https://fencemaker.app/api/v1/territory
Headers: X-API-Key: FENCEMAKER_KEY, Content-Type: application/json
Body: { "address": "CUSTOMER_ADDRESS" }
Response (matched):
{
  "matched": true,
  "territory_id": "uuid",
  "territory_code": "DEPOT-30MIN",
  "agent_id": null,
  "agent_name": null,
  "location_name": "Depot",
  "response_ms": 8
}
Response (no match):
{
  "matched": false,
  "territory_id": null,
  "response_ms": 7
}
Note: GET /pip and POST /territory return DIFFERENT response shapes β€” this is by design.
  GET /pip  β†’ result is inside "territory": { id, name, code, agent_id, ... }
  POST /territory β†’ result fields are at the top level: territory_id, territory_code, agent_id, ...
Your backend proxy must normalise these into one consistent shape before returning to your frontend.

## Step 5 β€” Show zone on map (Mapsi Tiles)
MapLibre: https://mapsi.dev/v1/tiles/styles?style=light&key=MAPSI_API_KEY
Add isochrone polygon as a GeoJSON fill layer. Show depot pin and customer pin.

## What to Generate
1. Setup script (run once): geocode depot β†’ generate isochrone β†’ POST to Fencemaker β†’ log territory_id
2. Checkout eligibility check (per customer):
   - Accept address string or lat/lon
   - If lat/lon available: GET /pip β†’ read result from territory.code, territory.id
   - If address string: POST /territory β†’ read result from territory_code, territory_id (top-level)
   - Backend proxy normalises both shapes into: { eligible: bool, zone_code, zone_name, territory_id }
   - Return normalised shape to frontend β€” never expose raw Fencemaker response shape differences
3. Checkout UI component:
   - Address input with Mapsi autocomplete (via your backend proxy)
   - "Check delivery availability" button
   - Result: "βœ“ We deliver to your address β€” 30 min drive time" or "βœ— Outside our delivery zone"
   - Show delivery zone map: isochrone polygon + customer pin (eligible = green, ineligible = red)
4. Backend proxy routes:
   GET /api/geocode?q=...           β†’ Mapsi /v1/geocode
   GET /api/isochrone?...           β†’ Mapsi /v1/isochrone
   POST /api/fencemaker/territories β†’ Fencemaker POST /api/v1/territories
   GET /api/fencemaker/pip?lat=&lon= β†’ Fencemaker GET /api/v1/pip
5. Admin panel endpoint: update drive time threshold (15/30/60 min) β†’ regenerate isochrone β†’ update Fencemaker territory

## Code Requirements
- Isochrone + Fencemaker territory registration happens at setup time, not per checkout
- Per-checkout: only ONE Fencemaker PIP call (fast β€” sub-20ms)
- Both API keys in environment variables: MAPSI_API_KEY and FENCEMAKER_API_KEY β€” never in client
- Handle Fencemaker 403 (territory limit reached on free plan) with clear error message
- Cache PIP result per customer address for 24 hours (delivery zones don't change per order)

## Where to Get Keys
- Mapsi key: https://mapsi.dev/console/api-keys
- Fencemaker key: https://fencemaker.app/keys

Generate the code now.
⚠️

Always review AI-generated code before shipping

Test against real Mapsi endpoints β€” response field names matter (coordinates.lat not geometry.location.lat). Confirm your API key is only in server-side code. Verify batch size limits match your plan. Run the API Playground to validate responses before wiring them to UI.

Tips for Better Output

πŸ“Œ Specify your framework upfront

"React with Next.js API routes" gets you server-side proxy code automatically. "Vanilla JS" gets you a different (simpler) pattern.

πŸ“‹ Ask for full files, not snippets

AI generates better code with full class and import context. Ask for "complete file" not "just the fetch call".

πŸ”— Name the APIs explicitly

Tell the AI "use Mapsi /v1/batch/reverse, not the single /v1/reverse endpoint" β€” AI will default to the simpler one otherwise.

πŸ” Iterate in phases

Get the geocoding working first, then add the map, then the proxy. Debugging one phase at a time is faster than debugging all at once.

Real-World Patterns β€” location ranking & zone logic

Where These Come From

These prompts are based on patterns from a real React + Vercel integration project: a pet care clinic finder that needed nearest-clinic ranking, a side-by-side fixed vs. drive-time zone comparison for the client to decide, and a postcode coverage check that returned a specific delivery fee tier. Generic tutorials don't cover these combinations β€” these prompts do.

14
Nearest Locations Ranker β€” Haversine vs Matrix Use Case

Rank nearby locations by distance first (Haversine, instant), then upgrade to drive-time ranking (Matrix API). Switchable modes with a toggle β€” devs often build one and miss the other.

/v1/geocode /v1/autocomplete /v1/matrix /v1/tiles/styles
β–Ό
I'm building a nearest-location finder with two ranking modes using Mapsi. Generate a complete implementation.
    
    ## Context
    - Framework: [React / Vue / Vanilla JS]
    - Backend: [Node.js / Python]
    - Use Case: User searches their address or uses current location, sees a list of nearby locations (clinics / stores / depots) ranked by proximity. Two modes: straight-line distance (fast) and actual drive time (accurate).
    - Location data: I have a static list of locations as JSON: [{ id, name, address, lat, lon }, ...]
    
    ## Why Two Modes
    - Haversine (straight-line): instant, no API call, good enough for initial display
    - Matrix (drive-time): accurate, costs 1 API call per search, surfaced as "Sort by drive time"
    - Pattern: show Haversine results immediately, let user upgrade to drive-time ranking on demand
    
    ## Mapsi APIs Used
    
    ### 1. Autocomplete + Geocode (get user's search coordinate)
    GET https://mapsi.dev/v1/autocomplete?text=PARTIAL&limit=5
    Headers: X-API-Key: YOUR_KEY  (server-side proxy)
    Response: { "suggestions": [{ "text": "...", "lat": 51.509, "lon": -0.118 }] }
    
    GET https://mapsi.dev/v1/geocode?q=ADDRESS&limit=1
    Headers: X-API-Key: YOUR_KEY
    Response: { "results": [{ "coordinates": { "lat": 51.509, "lon": -0.118 }, "formatted_address": "..." }] }
    
    ### 2. Matrix API (drive-time from user to all locations)
    POST https://mapsi.dev/v1/matrix
    Headers: X-API-Key: YOUR_KEY, Content-Type: application/json
    Body:
    {
      "sources": [{ "lat": USER_LAT, "lon": USER_LON }],
      "targets": [
        { "lat": 51.50, "lon": -0.10 },   // location 1
        { "lat": 51.52, "lon": -0.09 },   // location 2
        ...up to plan limit
      ],
      "profile": "car",
      "units": "metric"
    }
    Response:
    {
      "durations": [[480, 720, ...]],       // seconds from user to each target β€” durations[0][i]
      "distances": [[2300, 4100, ...]],     // metres
      "durations_text": [["8m", "12m", ...]]
    }
    sources has 1 entry (user). Result is a 1Γ—N matrix β€” durations[0][i] is travel time to location i.
    
    ## IMPORTANT β€” Matrix Plan Limits (sources Γ— targets, both dimensions capped independently)
    The matrix limit is NOT total pairs β€” it is the max for EACH dimension separately.
    In this use case: sources = 1 (the user), targets = your locations.
    The targets dimension determines how many locations you can rank per call:
    
      Free:       5Γ—5  β†’ max 5 targets  β†’ max 5 locations per Matrix call
      Growth:    10Γ—10 β†’ max 10 targets β†’ max 10 locations per Matrix call
      Business:  25Γ—25 β†’ max 25 targets β†’ max 25 locations per Matrix call
      Enterprise: Custom
    
    This means:
    - Free plan with 6+ locations: Matrix call will return a 400 error
    - Growth plan with 11+ locations: same
    - Always check location count against plan limit before calling Matrix
    
    Handling location counts that exceed your plan limit:
      Option A β€” Haversine pre-filter: rank all locations by Haversine first,
        take the nearest N (within your plan's target limit), then call Matrix
        on just those N. Result: accurate drive-time for the most plausible candidates.
      Option B β€” Haversine only: if location count exceeds limit, skip Matrix entirely
        and stay on Haversine mode. Show a note: "Drive-time ranking unavailable
        β€” too many locations for your current plan."
      Implement Option A β€” it gives the best user experience on all plans.
    
    ### 3. Map Tiles
    MapLibre: https://mapsi.dev/v1/tiles/styles?style=light&key=YOUR_API_KEY
    
    ## What to Generate
    
    ### Haversine Mode (default β€” no API call)
    1. haversineDistance(lat1, lon1, lat2, lon2) function β€” returns kilometres
       Formula: uses Earth radius 6371km, converts degrees to radians, standard haversine
    2. On search: geocode user address β†’ calculate haversine to every location β†’ sort ascending
    3. Display: ranked list with "X.X km away", top 3 highlighted
    
    ### Matrix Mode (on demand β€” with plan-aware pre-filtering)
    4. "Sort by drive time" toggle button β€” triggers Matrix API call
    5. Pre-filter step: sort all locations by Haversine, take nearest N based on plan:
       const MATRIX_TARGET_LIMIT = 5;  // set to match your Mapsi plan (5/10/25)
       const candidates = allLocations
         .map(loc => ({ ...loc, hDist: haversine(userLat, userLon, loc.lat, loc.lon) }))
         .sort((a, b) => a.hDist - b.hDist)
         .slice(0, MATRIX_TARGET_LIMIT);
    6. POST /api/matrix through your backend proxy:
       - sources: [{ lat: userLat, lon: userLon }]
       - targets: candidate location coordinates (max MATRIX_TARGET_LIMIT)
    7. Re-rank candidates by durations[0][i] ascending
    8. Display: ranked list with "X min drive" from durations_text[0][i]
       Note: only the top N candidates are drive-time ranked β€” remaining locations show
       Haversine distance with a "(straight-line)" label
    9. Loading state on toggle button while Matrix call is in flight
    
    ### Map
    10. MapLibre map: all location markers, user location marker (different colour)
    11. On list item hover: highlight corresponding marker, show popup with name + distance/time
    12. On list item click: map.flyTo() to that location
    
    ### UI
    13. Search bar with autocomplete dropdown (call your proxy /api/autocomplete)
    14. "Use my location" button: browser geolocation β†’ reverse geocode for display label
    15. Mode toggle: "πŸ“ By distance" (default) | "πŸš— By drive time" β€” clear visual state
    16. Result cards: rank number, location name, address, distance or drive time, "Get directions" link
    17. If Matrix unavailable (plan limit + no candidates): show toast "Showing straight-line distance"
    
    ## Code Requirements
    - MATRIX_TARGET_LIMIT must be a named constant β€” easy to update when plan upgrades
    - Run Haversine client-side (pure JS, no backend needed) β€” show results before any API call
    - Matrix call goes through backend proxy (X-API-Key must not be in browser)
    - Cache Matrix results per user coordinate (rounded to 3 decimal places) for 10 minutes
    - If Matrix call fails (400 target limit exceeded): fall back to Haversine mode silently,
      log the error server-side with the location count so you know when to upgrade plan
    - If Matrix call fails (429 or network error): stay on Haversine mode, show "Drive time unavailable" toast
    - "Get directions" link: https://www.google.com/maps/dir/?api=1&destination=LAT,LON
    
    Generate the code now.
15
Fixed Zone vs Isochrone β€” Side-by-Side Comparison Use Case

Build both zone types and present them in a split-screen toggle so a client or stakeholder can visually compare fixed GeoJSON polygons against Mapsi drive-time isochrones before deciding which to ship.

/v1/geocode /v1/isochrone /v1/tiles/styles GeoJSON polygons
β–Ό
I'm building a side-by-side comparison of fixed GeoJSON zones vs Mapsi isochrone zones so a stakeholder can choose which approach to use. Generate a complete implementation.
    
    ## Context
    - Framework: [React / Vanilla JS]
    - Use Case: We have two options for coverage zones β€” manually drawn postcode/boundary polygons (Fixed) and Mapsi drive-time isochrones (Dynamic). We need a comparison view to make the decision.
    - Decision criteria: Fixed = predictable, easy to explain to customers. Isochrone = accurate to actual drive time, updates if roads change.
    
    ## Zone Type A β€” Fixed GeoJSON Polygons
    Manually defined polygons stored as a GeoJSON FeatureCollection in your codebase.
    Each feature has properties: { name, tier, fee, color }
    Example structure:
    {
      "type": "FeatureCollection",
      "features": [
        {
          "type": "Feature",
          "properties": { "name": "Central Zone", "tier": 1, "fee": 50, "color": "#1a56db" },
          "geometry": { "type": "Polygon", "coordinates": [[[lon,lat], [lon,lat], ...]] }
        },
        {
          "type": "Feature",
          "properties": { "name": "Inner Zone", "tier": 2, "fee": 75, "color": "#7c3aed" },
          "geometry": { "type": "Polygon", "coordinates": [[[lon,lat], [lon,lat], ...]] }
        }
      ]
    }
    Note: Coordinates are [longitude, latitude] order in GeoJSON β€” not [lat, lon].
    
    ## Zone Type B β€” Mapsi Isochrone (Dynamic Drive-Time)
    GET https://mapsi.dev/v1/isochrone?lat=DEPOT_LAT&lon=DEPOT_LON&time=15,30&profile=car
    Headers: X-API-Key: YOUR_KEY  (server-side proxy)
    Parameters:
      time: comma-separated minutes β€” up to 5 contours e.g. "10,20,30"
      profile: car (default), bicycle, pedestrian
      denoise: 0.0–1.0 polygon smoothing (default 0.5)
    Response: GeoJSON FeatureCollection β€” one Polygon Feature per contour
      Each Feature has properties.contour_minutes
      Polygons are nested β€” 30-min polygon encloses 15-min polygon
    Pass directly to MapLibre as a GeoJSON source β€” no transformation needed.
    
    ## Map Tiles
    MapLibre: https://mapsi.dev/v1/tiles/styles?style=light&key=YOUR_API_KEY
    
    ## What to Generate
    
    ### Comparison Layout
    1. Two-panel layout OR single map with toggle:
       Option A (split-screen): two MapLibre maps side by side β€” left = Fixed, right = Isochrone
       Option B (single map toggle): one map, toggle button switches between zone layers
       Implement Option B (single map toggle) β€” simpler for mobile, same MapLibre instance
    
    2. Toggle control: "Fixed Zones" | "Drive-Time Zones" β€” pill toggle, clear active state
    
    ### Fixed Zone Panel
    3. Load GeoJSON FeatureCollection from local file or API endpoint
    4. Add to MapLibre as two sources (one per zone tier):
       map.addSource('fixed-zones', { type: 'geojson', data: fixedGeoJson })
    5. Fill layer per zone: 'fill' type, fill-color from feature properties.color, fill-opacity: 0.25
    6. Outline layer: 'line' type, 1.5px stroke matching feature color
    7. Click on zone: show popup with zone name and fee ("Central Zone β€” Β£50")
    8. Legend: colour-coded zone names with fee amounts
    
    ### Isochrone Panel
    9. On first switch to isochrone view: fetch from backend proxy GET /api/isochrone?lat=X&lon=Y&time=15,30
    10. Show loading spinner on map while fetching (isochrones can take 1-3 seconds)
    11. Add to MapLibre: same pattern β€” fill + outline layers, one per contour
    12. Colour by contour: 15-min = stronger opacity, 30-min = lighter
    13. Overlay depot marker (origin pin) on isochrone map
    14. map.fitBounds() to outer polygon on load
    
    ### Comparison UI
    15. Stats panel below map showing key differences:
        Fixed zones: "2 zones β€” defined by postcode boundaries β€” consistent shape"
        Isochrone:   "2 zones β€” defined by actual drive time β€” shape varies by traffic patterns"
    16. Pro/con list per approach (static content, not from API):
        Fixed: + Easy to explain, + Predictable boundaries, - Ignores road network
        Isochrone: + Accurate to real drive time, + Updates with road changes, - Shape less intuitive to customers
    17. "Choose this approach" button per mode β€” emits a decision event (onZoneTypeSelected('fixed' | 'isochrone'))
    
    ### Backend Proxy
    18. GET /api/isochrone?lat=X&lon=Y&time=X,Y β€” forwards to Mapsi with X-API-Key
    19. Cache isochrone result for 1 hour (same origin + time params = same result)
    
    ## Code Requirements
    - Both zone datasets loaded before map renders β€” no flicker on toggle
    - Toggle switches MapLibre layer visibility (setLayoutProperty 'visibility' 'visible'/'none') β€” faster than removing/adding layers
    - Fixed GeoJSON stored in /public/data/zones.geojson β€” easy for client to edit
    - Isochrone origin coordinate stored in env var (DEPOT_LAT, DEPOT_LON) β€” not hardcoded
    
    Generate the code now.
16
Coverage Check with Fee Tier Display Use Case

User enters address β†’ geocode β†’ point-in-polygon check against zone polygons β†’ return specific fee tier ("Β£50", "Β£75", "Not covered"). Works with either fixed GeoJSON or Fencemaker territories.

/v1/autocomplete /v1/geocode Turf.js PIP /v1/static-map
β–Ό
I'm building a coverage zone checker that returns a specific fee tier based on which zone a customer address falls in. Generate a complete implementation.
    
    ## Context
    - Framework: [React / Vue / Vanilla JS]
    - Backend: [Node.js / Python]
    - Use Case: Customer enters their address at checkout or on a landing page. System checks which coverage zone they're in and displays the corresponding fee (or "Not covered").
    - Zone data: [Fixed GeoJSON polygons stored locally] OR [Fencemaker territories via API]
    
    ## Fee Tier Configuration (your side β€” not from Mapsi)
    Map zone name/ID to a fee β€” store in your config:
      "Central Zone"  β†’ { fee: 50, currency: "Β£", label: "Express", eta: "Same day" }
      "Inner Zone"    β†’ { fee: 75, currency: "Β£", label: "Standard", eta: "Next day" }
      null (no match) β†’ { fee: null, label: "Not covered", eta: null }
    
    ## Mapsi APIs Used
    
    ### 1. Autocomplete (as user types)
    GET https://mapsi.dev/v1/autocomplete?text=PARTIAL&limit=5&layers=address
    Headers: X-API-Key: YOUR_KEY  (server-side proxy)
    Response: { "suggestions": [{ "text": "14 MG Road, London", "lat": 51.509, "lon": -0.118 }] }
    Use &layers=address to return address-level suggestions only (not city/country level)
    
    ### 2. Geocode (on selection or form submit)
    GET https://mapsi.dev/v1/geocode?q=ADDRESS&limit=1
    Response: { "results": [{ "coordinates": { "lat": 51.509, "lon": -0.118 }, "formatted_address": "..." }] }
    Note: if user selected from autocomplete, the lat/lon is already in the suggestion β€” skip geocode.
    
    ### 3. Point-in-Polygon check
    Option A β€” Client-side with Turf.js (for local GeoJSON zones):
      import * as turf from '@turf/turf'
      const point = turf.point([lon, lat])   // GeoJSON is [lon, lat] order
      const matched = zones.features.find(f => turf.booleanPointInPolygon(point, f))
      const zone = matched ? matched.properties : null
    
    Option B β€” Fencemaker API (for managed territories, more accurate for complex polygons):
      GET https://fencemaker.app/api/v1/pip?lat=LAT&lon=LON
      Headers: X-API-Key: FENCEMAKER_KEY  (server-side)
      Response: { "matched": true, "territory": { "name": "Central Zone", "code": "ZONE-A" } }
      OR: POST https://fencemaker.app/api/v1/territory  body: { "address": "ADDRESS" }
    
    ### 4. Static Map (visual confirmation β€” optional but high trust signal)
    GET https://mapsi.dev/v1/static-map?lat=LAT&lon=LON&zoom=13&width=400&height=200
    Headers: X-API-Key: YOUR_KEY
    Returns PNG β€” show as inline confirmation map with customer pin
    
    ## What to Generate
    
    ### Address Input Component
    1. Text input with autocomplete dropdown (call /api/autocomplete proxy, debounce 300ms)
    2. "Use my location" button β€” browser geolocation β†’ reverse geocode for display
    3. On selection: extract lat/lon from suggestion (or geocode if typed manually)
    
    ### Coverage Check Logic
    4. checkCoverage(lat, lon) function:
       - Option A (Turf.js): load zones GeoJSON, run booleanPointInPolygon against each zone
         - Check zones in order from smallest to largest (innermost match wins if zones overlap)
       - Option B (Fencemaker): POST to /api/fencemaker/pip backend proxy
       - Return: { matched: bool, zoneName, fee, currency, label, eta }
    
    5. Backend proxy for Option B:
       GET /api/coverage-check?lat=X&lon=Y
       β†’ calls Fencemaker GET /api/v1/pip
       β†’ maps territory.name to fee tier from your config
       β†’ returns { matched, fee, label, eta }
    
    ### Result Display
    6. Three result states β€” clear, distinct UI for each:
    
       βœ… COVERED (inner zone):
       "We cover your area Β· [Zone Label]"
       Fee badge: "Β£50" (large, prominent)
       ETA badge: "Same day delivery"
       Small inline static map showing customer pin inside zone polygon
    
       βœ… COVERED (outer zone):
       Same pattern, different fee: "Β£75 Β· Next day delivery"
    
       ❌ NOT COVERED:
       "Sorry, we don't cover this area yet"
       "Nearest covered area: [Zone name] β€” approx X km away" (haversine to zone centroid)
       Optional: email waitlist capture form
    
    7. Trust signals on covered result:
       - Static map: GET /v1/static-map?lat=X&lon=Y&zoom=13&width=400&height=200
       - Displayed as confirmation image: "Your location is in our [Zone] coverage area"
    
    ### Caching
    8. Cache coverage check result per address string for 24 hours (sessionStorage)
       Key: md5 or btoa(address), value: { matched, fee, label, checkedAt }
       On repeat check: show cached result instantly, refresh in background
    
    ## Code Requirements
    - For Option A (Turf.js): npm install @turf/turf β€” import only booleanPointInPolygon to keep bundle small
    - Check zones from smallest area to largest β€” if user is in both Central and Inner, Central wins
    - Static map img tag: use server proxy endpoint to avoid exposing X-API-Key in browser
      /api/static-map?lat=X&lon=Y&zoom=13&width=400&height=200 β†’ proxies to Mapsi with key
    - Fee amounts and zone names come from YOUR config β€” never from Mapsi/Fencemaker API
      (API returns zone identity; you map identity to fee)
    - Handle edge case: address geocodes successfully but lands exactly on a zone boundary
      (turf.js treats boundary as inside β€” document this behaviour)
    
    Generate the code now.
⚠️

Always review AI-generated code before shipping

Verify Turf.js coordinate order β€” GeoJSON expects [longitude, latitude], not [lat, lon]. Test zone boundary edge cases on real addresses. Confirm fee amounts come from your config, not the API. Run the API Playground to validate responses before wiring to UI.

Inline IDE Shortcut

Paste this at the top of any file to give GitHub Copilot or Cursor instant Mapsi context:

// Mapsi Mapping & Geocoding API Integration
// Base URL: https://mapsi.dev
// Auth: X-API-Key header (server-side for all except tiles)
// Tiles: ?key= URL param β€” browser-safe exception
//
// Key endpoints:
// GET  /v1/geocode?q=ADDRESS&limit=1           β†’ { results[].coordinates{lat,lon}, formatted_address }
// GET  /v1/autocomplete?text=PARTIAL&limit=5   β†’ { suggestions[].{text,lat,lon} }
// GET  /v1/reverse?lat=X&lon=Y                 β†’ { results[].formatted_address }
// GET  /v1/places?q=QUERY&lat=X&lon=Y          β†’ { places[].{name,lat,lon} }
// POST /v1/route  body:{waypoints:[{lat,lon}],mode:"auto",units:"km"} β†’ {distance_km,duration_sec,polyline}
// POST /v1/matrix body:{sources,targets,profile} β†’ {durations[][],distances[][],durations_text[][]}
// GET  /v1/isochrone?lat=X&lon=Y&time=15,30    β†’ GeoJSON FeatureCollection of reachability polygons
// GET  /v1/static-map?lat=X&lon=Y&zoom=15&width=600&height=400 β†’ PNG
// GET  /v1/tiles/styles?style=light&key=KEY    β†’ MapLibre style URL (pass to new maplibregl.Map({style:url}))
// POST /v1/batch/geocode body:{addresses:[]}   β†’ array of geocode results
// POST /v1/batch/reverse body:{points:[{lat,lon,id}]} β†’ {results[].{id,address}}
// GET  /v1/pip?lat=X&lon=Y                     β†’ {country,state,city,neighbourhood,hierarchy}
// GET  /v1/timezone?lat=X&lon=Y                β†’ {timezone,utc_offset,dst,current_time}
// POST /v1/normalize body:{address:""}         β†’ {parsed:{house_number,road,city,postcode},normalized:[]}
// POST /v1/elevation body:{points:[{lat,lon}]} β†’ {results[].{elevation_m}}
// POST /v1/match body:{points:[{lat,lon,timestamp}],profile:"car"} β†’ {confidence,geometry,matched_points}
// GET  /v1/nearest-road?lat=X&lon=Y            β†’ {results[].{road_name,highway,distance_m,snapped_point}}
//
// Use case: [YOUR_USE_CASE]