git clone https://github.com/ComeOnOliver/skillshub
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/TerminalSkills/skills/maps-geolocation" ~/.claude/skills/comeonoliver-skillshub-maps-geolocation && rm -rf "$T"
skills/TerminalSkills/skills/maps-geolocation/SKILL.mdMaps & 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: '© 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
objects, Mapbox and GeoJSON use{lat, lng}
arrays; mixing these up is the most common source of misplaced markers.[lng, lat]