Maps Integration skill for Codex

Fast maps, restricted keys, coordinates the right way round. Codex chooses a map provider from licence, data and budget needs, then renders locations as GeoJSON sources and clustered layers instead of thousands of DOM markers. Browser and server keys get separate restrictions, geocoding runs server-side with rate limits and confidence flags, and storage of results follows each provider's caching terms.

  • Integrations
  • $maps-integration
  • 159 lines
  • 2 files
  • ~2,000 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- maps-integration

Installs to ~/.agents/skills/maps-integration/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $maps-integration, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Adds interactive maps and geocoding to web apps with MapLibre GL JS, Mapbox GL JS or the Google Maps JavaScript API - provider choice, key restriction, GeoJSON sources and layers, clustering thousands of points, forward and reverse geocoding with caching rules, and React wrappers. Use when a user asks to show locations on a map, add a store locator, geocode addresses, draw routes or areas, fix slow marker rendering, or switch map providers.

Use it when

  • Showing locations, routes or areas on an interactive web map
  • Geocoding or reverse geocoding addresses in an app or batch job
  • A map with many markers is slow or a provider bill is climbing
  • Switching between Google Maps, Mapbox and MapLibre

Use something else when

  • You are analysing spatial data in Python or SQL rather than displaying it: use geospatial-analysis
  • You need custom 3D scenes rather than geographic maps: use threejs-scenes

How to check the result

A known address lands on the right block, a production-sized dataset pans smoothly, and the browser key fails when called from an origin it is not restricted to.

The mistake it prevents

Passing [lat, lng] where GeoJSON and MapLibre expect [lng, lat], which drops points into the ocean.

~/.agents/skills/maps-integration/SKILL.md

--- name: maps-integration description: Adds interactive maps and geocoding to web apps with MapLibre GL JS, Mapbox GL JS or the Google Maps JavaScript API - provider choice, key… license: MIT ---

Done well means: the provider fits the licence and budget, keys are restricted so a leaked key is useless elsewhere, points render as data layers (not thousands of DOM nodes), coordinates are never swapped, and geocoding results are cached only as the provider’s terms allow.

Inspect first

  1. Read AGENTS.md, then rg -n "maplibre|mapbox|google.maps|leaflet|react-map-gl|@vis.gl" --glob '!node_modules'.
  2. Note where keys come from (NEXT_PUBLIC_*, VITE_*) and whether a CSP restricts connect-src, worker-src and img-src; map libraries need tile, style and worker origins allowed.
  3. Ask for the expected scale: number of points, map loads per month, geocodes per month. Billing is per map load or per request on Mapbox and Google.

Choose a provider

SituationDefault
Vector maps, no vendor lock-in, own or third-party tiles (MapTiler, Stadia, Protomaps, OpenFreeMap)MapLibre GL JS
Want Mapbox styles, Standard 3D style, Mapbox Search / DirectionsMapbox GL JS v3 (proprietary licence, needs token)
Need Google Places data, Street View, or users expect Google’s mapGoogle Maps JavaScript API
Simple raster map, tiny bundle, few markersLeaflet
Large analytic overlays (100k+ points, heatmaps, arcs)deck.gl on top of any of the above

Do not point a production app at tile.openstreetmap.org; the OSM tile usage policy forbids heavy use. Use a tile provider or self-host PMTiles.

Keys and restrictions

  • Mapbox: public pk. token in the browser with URL restrictions set in the account; secret sk. tokens only on servers (uploads, tilesets).
  • Google: one browser key restricted by HTTP referrer and to the Maps JavaScript API only; a separate server key restricted by IP and to Geocoding / Places for backend calls.
  • Set budgets and quota caps in the provider console. Changing billing settings is the user’s call; list what to change rather than doing it.

MapLibre / Mapbox: data as sources and layers

import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';

const map = new maplibregl.Map({
  container: 'map',
  style: `https://api.maptiler.com/maps/streets-v2/style.json?key=${import.meta.env.VITE_MAPTILER_KEY}`,
  center: [-73.985, 40.748],   // [lng, lat]
  zoom: 11,
});

map.on('load', () => {
  map.addSource('stores', {
    type: 'geojson',
    data: '/api/stores.geojson',
    cluster: true,
    clusterRadius: 50,
    clusterMaxZoom: 14,
  });
  map.addLayer({ id: 'clusters', type: 'circle', source: 'stores', filter: ['has', 'point_count'],
    paint: { 'circle-radius': ['step', ['get', 'point_count'], 16, 50, 22, 200, 30], 'circle-color': '#2563eb' } });
  map.addLayer({ id: 'cluster-count', type: 'symbol', source: 'stores', filter: ['has', 'point_count'],
    layout: { 'text-field': ['get', 'point_count_abbreviated'], 'text-size': 12 } });
  map.addLayer({ id: 'store', type: 'circle', source: 'stores', filter: ['!', ['has', 'point_count']],
    paint: { 'circle-radius': 6, 'circle-color': '#111827' } });

  map.on('click', 'store', (e) => {
    const f = e.features![0];
    new maplibregl.Popup().setLngLat(f.geometry.coordinates).setText(f.properties.name).addTo(map);
  });
});

Mapbox GL JS is nearly identical: mapboxgl.accessToken = token, style: 'mapbox://styles/mapbox/standard'.

Rules:

  • Up to a few hundred interactive pins: DOM Marker is fine. Beyond that use a circle or symbol layer; DOM markers cost a node each and stall panning.
  • Update data with map.getSource('stores').setData(geojson); do not remove and re-add layers.
  • Add sources and layers inside load (or style.load if you switch styles at runtime, because a style change drops custom layers).
  • Call map.remove() on unmount in SPAs to free the WebGL context; browsers cap active contexts.

Google Maps JavaScript API

Load with the official dynamic bootstrap (or @googlemaps/js-api-loader) and import libraries on demand:

const { Map } = await google.maps.importLibrary('maps') as google.maps.MapsLibrary;
const { AdvancedMarkerElement } = await google.maps.importLibrary('marker') as google.maps.MarkerLibrary;

const map = new Map(document.getElementById('map')!, {
  center: { lat: 40.748, lng: -73.985 },   // object, lat first
  zoom: 11,
  mapId: import.meta.env.VITE_GOOGLE_MAP_ID,   // required for AdvancedMarkerElement
});
new AdvancedMarkerElement({ map, position: { lat: 40.748, lng: -73.985 }, title: 'Store 1' });
  • google.maps.Marker is deprecated; use AdvancedMarkerElement, which needs a Map ID.
  • For many markers use @googlemaps/markerclusterer, or a deck.gl overlay for very large sets.
  • React: @vis.gl/react-google-maps (APIProvider, Map, AdvancedMarker). For MapLibre or Mapbox, react-map-gl exposes both via react-map-gl/maplibre and react-map-gl/mapbox.

Geocoding

ProviderEndpointCaching rule of thumb
MapboxGET https://api.mapbox.com/search/geocode/v6/forward?q=...&access_token=...Storing results requires permanent geocoding (permanent=true, billed differently)
GoogleGET https://maps.googleapis.com/maps/api/geocode/json?address=...&key=...Terms restrict long-term caching of coordinates; place IDs may be stored
Nominatim (OSM)GET https://nominatim.openstreetmap.org/search?q=...&format=jsonv2Max 1 request/second, identifying User-Agent, no bulk jobs; self-host for volume

Read the current terms before persisting geocodes; they change and differ by product.

Workflow for address data:

  1. Normalise input (trim, collapse whitespace, country hint) and pass a country or proximity bias.
  2. Geocode on the server with the server key; batch jobs run in a queue with rate limiting and exponential backoff on 429.
  3. Store result plus confidence/location_type, provider, and timestamp. Flag low-confidence matches (APPROXIMATE, RANGE_INTERPOLATED, Mapbox match_code not exact) for review.
  4. For autocomplete in the browser, debounce 250-300 ms, require 3+ characters, and use session tokens where the provider bills per session (Google Places Autocomplete, Mapbox Search Box).

Geometry and distance

  • GeoJSON and MapLibre/Mapbox use [longitude, latitude]; Google uses { lat, lng }; many databases (PostGIS ST_MakePoint(lng, lat)) use longitude first. Swapped coordinates usually put points in the ocean or Antarctica; check one known location.
  • Do “nearest store” queries in the database (PostGIS ST_DWithin on geography, or a geohash index), not by downloading every point to the browser.
  • Use @turf/turf for client-side buffers, bounding boxes and point-in-polygon.
  • Fit the viewport with map.fitBounds(bbox, { padding: 40 }) after loading data.

Verification

  • One known address geocodes to the expected coordinates within ~50 m and renders on the right block.
  • Load a production-sized dataset (e.g. 10k points): pan and zoom stay smooth; no long tasks over 200 ms in the Performance panel.
  • The key works on the allowed origin and fails from another origin or with curl without a referrer.
  • No console errors for blocked workers or tiles (CSP), and attribution is visible as the licence requires.
  • Unmounting and remounting the map ten times does not leak WebGL contexts.

Gotchas

  • Hidden containers (tabs, modals) initialise with zero size; call map.resize() when shown.
  • Missing the library CSS breaks popups and controls in confusing ways.
  • Attribution must stay visible; hiding it violates most tile and data licences.
  • Server-side rendering: map libraries touch window; load them client-only (dynamic(..., { ssr: false }) in Next.js).
  • A style URL with a key in it is public; restrict that key like any other browser key.

Output

Report: provider and why, keys needed with their restrictions, data path (source -> API -> layer), estimated monthly loads and geocodes, and the checks run.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Maps Integration"
  short_description: "Fast maps, restricted keys, coordinates the right way round"
  default_prompt: "$maps-integration Add a store locator map with clustering for our 8,000 locations"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc