skills/terminalskills/skills/maps-geolocation

maps-geolocation

SKILL.md

Maps & Geolocation

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 ($200/month free credit):

# Enable: Maps JS API, Geocoding API, Directions API, Places API, Distance Matrix API
export GOOGLE_MAPS_API_KEY="AIzaSy..."

Mapbox (50,000 free map loads/month):

export MAPBOX_ACCESS_TOKEN="pk.eyJ1..."

Leaflet + OpenStreetMap (completely free, no API key):

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9/dist/leaflet.js"></script>

Nominatim (free geocoding, 1 req/sec rate limit, no key needed).

Step 2: Geocoding & Reverse Geocoding

// Google Geocoding
async function geocode(address: string) {
  const res = await fetch(
    `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${API_KEY}`
  );
  const data = await res.json();
  const r = data.results[0];
  return { lat: r.geometry.location.lat, lng: r.geometry.location.lng, formatted: r.formatted_address };
}

// Nominatim (free, no key — requires User-Agent header per TOS)
async function nominatimGeocode(query: string) {
  const res = await fetch(
    `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=5`,
    { headers: { "User-Agent": "MyApp/1.0 (contact@myapp.com)" } }
  );
  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;
}

Step 3: Interactive Maps

Leaflet + OpenStreetMap (free):

const map = L.map("map").setView([48.8566, 2.3522], 12);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
  attribution: '&copy; OpenStreetMap 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);
});

Mapbox GL JS (vector tiles, custom styles):

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 Directions
async function getRoute(origin: string, destination: string, mode = "driving") {
  const res = await fetch(
    `https://maps.googleapis.com/maps/api/directions/json?origin=${encodeURIComponent(origin)}&destination=${encodeURIComponent(destination)}&mode=${mode}&key=${API_KEY}`
  );
  const data = await res.json();
  const leg = data.routes[0].legs[0];
  return { distance: leg.distance.text, duration: leg.duration.text, polyline: data.routes[0].overview_polyline.points };
}

// Distance Matrix (many-to-many)
async function distanceMatrix(origins: string[], destinations: string[]) {
  const res = await fetch(
    `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${origins.map(encodeURIComponent).join("|")}&destinations=${destinations.map(encodeURIComponent).join("|")}&key=${API_KEY}`
  );
  return res.json();
}

// OSRM (free, no key)
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 (Google Directions with waypoint reordering):

async function optimizeRoute(origin: string, destination: string, waypoints: string[]) {
  const res = await fetch(
    `https://maps.googleapis.com/maps/api/directions/json?origin=${encodeURIComponent(origin)}&destination=${encodeURIComponent(destination)}&waypoints=optimize:true|${waypoints.map(encodeURIComponent).join("|")}&key=${API_KEY}`
  );
  const route = (await res.json()).routes[0];
  return {
    optimizedOrder: route.waypoint_order,
    totalDistance: route.legs.reduce((s: number, l: any) => s + l.distance.value, 0),
    totalDuration: route.legs.reduce((s: number, l: any) => s + l.duration.value, 0),
  };
}

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 free) 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 Directions API with waypoints=optimize: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 charges per API call after $200/month free credit; Mapbox offers 50,000 free map loads; Leaflet + OSM + Nominatim is entirely free but rate-limited to 1 request/second.
  • 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) 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.
Weekly Installs
2
GitHub Stars
15
First Seen
8 days ago
Installed on
amp2
cline2
opencode2
cursor2
kimi-cli2
codex2