Overview
Build location-based applications using mapping and geolocation APIs. This skill covers four major platforms — Google Maps Platform, Mapbox, Leaflet (open-source), and OpenStreetMap/Nominatim (free) — for geocoding, routing, interactive maps, geofencing, heatmaps, store locators, fleet tracking, and address autocomplete. Choose based on budget: Google Maps for full-featured commercial use, Mapbox for custom styling, Leaflet+OSM for zero-cost self-hosted solutions.
Instructions
Step 1: Platform Selection & Setup
Google Maps Platform (billing account required; since March 2025 the flat $200 credit is replaced by a free monthly allowance per SKU, about 10,000 calls for Essentials SKUs such as Geocoding, 5,000 for Pro, 1,000 for Enterprise; check the pricing page for your SKUs):
# Enable in the Cloud console: Maps JavaScript API, Geocoding API, Routes API, Places API (New)
export GOOGLE_MAPS_API_KEY="AIzaSyD-example-restrict-this-key"
Directions API, Distance Matrix API and the old Places API are marked Legacy; use the Routes API (Step 4) for new projects. Restrict the key (HTTP referrers for browser keys, IP addresses or API restrictions for server keys).
Mapbox (free monthly tier: 50,000 GL JS map loads, 100,000 geocoding requests, 100,000 Directions requests; verify on the pricing page):
export MAPBOX_ACCESS_TOKEN="pk.eyJ1..."
Leaflet + OpenStreetMap (completely free, no API key):
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
Or npm install leaflet. Leaflet 1.9.4 is the stable release; 2.x is still alpha.
Nominatim (free geocoding, max 1 request/second, no key, but a User-Agent or Referer that identifies your app is mandatory; autocomplete-as-you-type and bulk geocoding are not allowed on the public server, and results must be cached. For heavy use self-host Nominatim or use a commercial geocoder). The OSM Foundation's policy (operations.osmfoundation.org/policies/nominatim) also says code generated by an LLM must follow it, so read it before shipping.
Step 2: Geocoding & Reverse Geocoding
// Google Geocoding (v3 JSON endpoint; still served, see the v4 note below)
async function geocode(address: string) {
const res = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${process.env.GOOGLE_MAPS_API_KEY}`
);
const data = await res.json();
// HTTP status is 200 even on errors: check the JSON status field
if (data.status !== "OK") throw new Error(`Geocoding failed: ${data.status}`);
const r = data.results[0];
return { lat: r.geometry.location.lat, lng: r.geometry.location.lng, formatted: r.formatted_address };
}
// Nominatim (free, no key; usage policy applies: identifying User-Agent, 1 request/second, cache results)
async function nominatimGeocode(query: string) {
const res = await fetch(
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=5`,
{ headers: { "User-Agent": "StoreLocator/1.0 (https://brewline-coffee.dev)" } }
);
return res.json();
}
// Batch geocoding with rate limiting
async function batchGeocode(addresses: string[], delayMs = 100) {
const results = [];
for (const addr of addresses) {
try { results.push({ address: addr, ...await geocode(addr) }); }
catch (err) { results.push({ address: addr, lat: null, lng: null, error: err.message }); }
await new Promise(r => setTimeout(r, delayMs));
}
return results;
}
Geocoding API v4 is now generally available and is the successor to the v3 /maps/api/geocode/json endpoint. It is a REST API on geocode.googleapis.com: address geocoding is GET https://geocode.googleapis.com/v4/geocode/address/{address} with the key in the X-Goog-Api-Key header and an X-Goog-FieldMask header, place-id lookups use /v4/geocode/places/{place}, and there is a destinations search for building entrances. Parameters are renamed (language to languageCode, region to regionCode), the status field is gone (errors use HTTP status codes), and Google says v4 is server-to-server only, so call it from your backend, never from browser code. For new server-side projects use v4; existing v3 code keeps working.
Step 3: Interactive Maps
Leaflet + OpenStreetMap (free):
const map = L.map("map").setView([48.8566, 2.3522], 12);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
L.marker([48.8584, 2.2945]).addTo(map)
.bindPopup("<b>Eiffel Tower</b><br>Paris, France").openPopup();
// Load GeoJSON layer
fetch("/data/zones.geojson").then(r => r.json()).then(data => {
L.geoJSON(data, {
style: { color: "#ef4444", weight: 2, fillOpacity: 0.1 },
onEachFeature: (feature, layer) => layer.bindPopup(feature.properties.name),
}).addTo(map);
});
The public OSM tile server is for light interactive use only (no bulk download or offline caching); for production traffic use a tile provider or self-host.
Mapbox GL JS (vector tiles, custom styles; current major is v3):
mapboxgl.accessToken = "pk.eyJ1...";
const map = new mapboxgl.Map({
container: "map", style: "mapbox://styles/mapbox/streets-v12",
center: [2.3522, 48.8566], zoom: 12,
});
new mapboxgl.Marker({ color: "#ef4444" })
.setLngLat([2.2945, 48.8584])
.setPopup(new mapboxgl.Popup().setHTML("<h3>Eiffel Tower</h3>"))
.addTo(map);
map.addControl(new mapboxgl.NavigationControl());
Step 4: Routing & Distance Matrix
// Google Routes API (replaces the Directions and Distance Matrix APIs).
// POST + JSON body; the X-Goog-FieldMask header is mandatory and you pay for the fields you ask for.
const ROUTES = "https://routes.googleapis.com";
async function getRoute(origin: string, destination: string, travelMode = "DRIVE") {
const res = await fetch(`${ROUTES}/directions/v2:computeRoutes`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": process.env.GOOGLE_MAPS_API_KEY!,
"X-Goog-FieldMask": "routes.distanceMeters,routes.duration,routes.polyline.encodedPolyline",
},
body: JSON.stringify({ origin: { address: origin }, destination: { address: destination }, travelMode }),
});
const route = (await res.json()).routes?.[0];
if (!route) throw new Error("No route found");
// duration is a string such as "1560s"
return { meters: route.distanceMeters, seconds: parseInt(route.duration), polyline: route.polyline.encodedPolyline };
}
// Many-to-many matrix: returns elements with originIndex, destinationIndex, distanceMeters, duration
async function distanceMatrix(origins: string[], destinations: string[]) {
const res = await fetch(`${ROUTES}/distanceMatrix/v2:computeRouteMatrix`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": process.env.GOOGLE_MAPS_API_KEY!,
"X-Goog-FieldMask": "originIndex,destinationIndex,distanceMeters,duration,status,condition",
},
body: JSON.stringify({
origins: origins.map(address => ({ waypoint: { address } })),
destinations: destinations.map(address => ({ waypoint: { address } })),
travelMode: "DRIVE",
}),
});
return res.json();
}
// OSRM public demo server (free, no key, demo use only: self-host OSRM or use a hosted router in production).
// Coordinates are [lng, lat].
async function osrmRoute(coords: [number, number][]) {
const wp = coords.map(c => c.join(",")).join(";");
const res = await fetch(`https://router.project-osrm.org/route/v1/driving/${wp}?overview=full&geometries=geojson`);
const data = await res.json();
return { distance: data.routes[0].distance, duration: data.routes[0].duration, geometry: data.routes[0].geometry };
}
Step 5: Geofencing & Utilities
// Haversine distance (meters)
function haversineDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
const R = 6371000;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLng = ((lng2 - lng1) * Math.PI) / 180;
const a = Math.sin(dLat / 2) ** 2 +
Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// Point-in-polygon check
function pointInPolygon(point: [number, number], polygon: [number, number][]): boolean {
const [x, y] = point;
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const [xi, yi] = polygon[i], [xj, yj] = polygon[j];
if ((yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside;
}
return inside;
}
// Circular geofence check
function isInRadius(point: [number, number], center: [number, number], radiusMeters: number): boolean {
return haversineDistance(point[0], point[1], center[0], center[1]) <= radiusMeters;
}
Step 6: Store Locator & Route Optimization
Nearby search with PostGIS:
SELECT id, name, address, lat, lng,
ST_DistanceSphere(ST_MakePoint(lng, lat), ST_MakePoint($1, $2)) AS distance_meters
FROM stores
WHERE ST_DWithin(ST_MakePoint(lng, lat)::geography, ST_MakePoint($1, $2)::geography, $3)
ORDER BY distance_meters LIMIT $4;
Route optimization (Routes API reorders intermediate stops; for large fleets with time windows and capacities use the Route Optimization API or an open-source solver such as VROOM):
async function optimizeRoute(origin: string, destination: string, stops: string[]) {
const res = await fetch("https://routes.googleapis.com/directions/v2:computeRoutes", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": process.env.GOOGLE_MAPS_API_KEY!,
"X-Goog-FieldMask": "routes.optimizedIntermediateWaypointIndex,routes.distanceMeters,routes.duration",
},
body: JSON.stringify({
origin: { address: origin },
destination: { address: destination },
intermediates: stops.map(address => ({ address })),
travelMode: "DRIVE",
optimizeWaypointOrder: true,
}),
});
const route = (await res.json()).routes[0];
return {
optimizedOrder: route.optimizedIntermediateWaypointIndex, // indexes into stops
totalMeters: route.distanceMeters,
totalSeconds: parseInt(route.duration),
};
}
Examples
Example 1: Store locator with address search for a coffee chain
User prompt: "Build a store locator for our coffee shops. The user types an address, we geocode it, find the 5 nearest stores within 10km from our PostgreSQL database, and display them on a Leaflet map with distance labels."
The agent will create a geocoding function using Google Geocoding API (or Nominatim for low-volume, cached lookups within its usage policy) to convert the user's address input to coordinates. It will write a PostGIS SQL query using ST_DWithin to find the 5 nearest stores within 10,000 meters, returning name, address, coordinates, and distance. On the frontend, it will initialize a Leaflet map centered on the searched location, add a marker for each store with a popup showing name, address, and distance in km, and fit the map bounds to show all results.
Example 2: Delivery fleet route optimization with geofence alerts
User prompt: "Optimize the delivery route for a driver starting from our Berlin warehouse with 6 drop-off addresses, then set up a 200m geofence around each stop that logs arrival timestamps."
The agent will call the Google Routes API (computeRoutes with optimizeWaypointOrder: true) to reorder the 6 stops for minimum travel time, returning the optimal sequence, total distance, and estimated duration. It will then create a geofence monitor with a 200-meter circular zone around each stop using the haversine distance function, checking incoming GPS coordinates against each fence and logging enter/exit events with timestamps to track delivery progress.
Guidelines
- Choose the right platform for cost — Google Maps bills per SKU after a small monthly free allowance; Mapbox has a free monthly tier (50,000 map loads); Leaflet + OSM + Nominatim has no fee but the public servers have strict usage policies (1 request/second, no bulk or offline use).
- Always include attribution — OpenStreetMap requires visible attribution on rendered maps; Mapbox and Google have their own attribution requirements that must not be removed.
- Rate-limit geocoding requests — add delays between batch geocoding calls (100ms+ for Google, 1 second for Nominatim, which also forbids bulk jobs on the public server) to avoid hitting rate limits and getting blocked.
- Close GeoJSON polygon rings — the first and last coordinate in a polygon must be identical; omitting this causes rendering failures and invalid geometry errors across all platforms.
- Cache geocoding results — store lat/lng in your database after the first lookup to avoid repeated API calls for the same addresses, which both saves cost and improves response times.
- Validate coordinate order — Google Maps uses
{lat, lng}objects, Mapbox and GeoJSON use[lng, lat]arrays; mixing these up is the most common source of misplaced markers. - Autocomplete — Google's
google.maps.places.Autocompletewidget is Legacy; new code usesPlaceAutocompleteElement(await google.maps.importLibrary("places")) or Places API (New) autocomplete requests. Nominatim cannot be used for type-ahead. - Protect keys — never ship an unrestricted Google key or a Mapbox secret token (
sk.) to the browser; browser code uses restricted keys or publicpk.tokens only.