How to Add Geocoding to Your App with Mapbox 2026
Mapbox Geocoding converts addresses to coordinates and coordinates to addresses. Combined with Mapbox GL, you get autocomplete search, interactive maps, and precise location data. This guide covers the full integration.
TL;DR
Mapbox Geocoding API delivers sub-100ms responses for 195+ countries. The free tier covers 100,000 requests/month — enough for most apps in early production. Choose Mapbox over Google Maps Geocoding when you need custom map styling, open-source flexibility via MapLibre, or lower costs at scale. The Places API structure (address, POI, neighborhood, city, region, country) maps cleanly to real-world search UX patterns, and the autocomplete experience is competitive with Google's. If you're already using Mapbox GL for rendering, consolidating on Mapbox for geocoding avoids a second vendor relationship entirely.
What You'll Build
- Forward geocoding (address → coordinates)
- Reverse geocoding (coordinates → address)
- Autocomplete search input
- Map with geocoded markers
- Batch geocoding for multiple addresses
Prerequisites: React/Next.js, Mapbox account (free: 100K geocoding requests/month).
Geocoding is the backbone of a surprising number of product features. Store finders need to convert a user's typed address into a map pin. Delivery apps need to validate addresses at checkout and then convert GPS pings back to human-readable addresses during tracking. Job boards, real estate platforms, and event apps all use it to filter by proximity. Even simple profile forms use reverse geocoding to auto-populate city and country fields from a browser's Geolocation API. This guide builds each of those capabilities one function at a time, starting with the simplest case (a single address lookup) and ending with a server-side batch processor for bulk data enrichment.
1. Setup
Get Access Token
- Sign up at mapbox.com
- Go to Account → Access Tokens
- Copy your default public token
NEXT_PUBLIC_MAPBOX_TOKEN=pk.eyJ1...
Install
npm install mapbox-gl @mapbox/mapbox-gl-geocoder
npm install -D @types/mapbox-gl
2. Forward Geocoding
Forward geocoding takes a string — a business name, street address, neighborhood, or city — and returns coordinates plus structured place data. Mapbox organizes results into a hierarchy of place types: address (specific street addresses), poi (points of interest like businesses), neighborhood, place (cities and towns), region (states and provinces), and country. This hierarchy matters when you're building autocomplete: you'll often want to scope searches to address and poi types to avoid cluttering results with continent-level matches when a user is typing a specific street address. The limit parameter defaults to 5 and can go up to 10. For most autocomplete UIs, 5 is enough — more results increase cognitive load without improving conversion.
// lib/mapbox.ts
const MAPBOX_TOKEN = process.env.NEXT_PUBLIC_MAPBOX_TOKEN!;
export interface GeocodingResult {
placeName: string;
coordinates: [number, number]; // [lng, lat]
type: string; // 'address', 'poi', 'place', 'region', 'country'
}
export async function geocode(query: string): Promise<GeocodingResult[]> {
const res = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(query)}.json?access_token=${MAPBOX_TOKEN}&limit=5`
);
const data = await res.json();
return data.features.map((f: any) => ({
placeName: f.place_name,
coordinates: f.center as [number, number],
type: f.place_type[0],
}));
}
3. Reverse Geocoding
Reverse geocoding takes a longitude/latitude pair and returns a human-readable place name. The two most common triggers are: a user clicking or tapping on a map (you have coordinates, you need an address label), and a device's Geolocation API returning a GPS fix (you have coordinates, you need a city and country to display in the UI). In both cases, you typically only want the single best match, so limit=1 is appropriate. The response will include the full place hierarchy — you can drill into context on the feature object to extract just the city or just the country if you need structured components rather than the full formatted address string.
export async function reverseGeocode(lng: number, lat: number): Promise<string> {
const res = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${lng},${lat}.json?access_token=${MAPBOX_TOKEN}&limit=1`
);
const data = await res.json();
return data.features[0]?.place_name ?? 'Unknown location';
}
4. Autocomplete Search
Autocomplete is where geocoding UX lives or dies. The two most common mistakes are firing a request on every keystroke (burning through your quota and overwhelming the API) and waiting too long before firing (making the UI feel sluggish). A 300ms debounce after the last keystroke is the industry-standard sweet spot: fast enough to feel responsive on a wired connection, conservative enough to avoid wasting requests on intermediate characters. The minimum query length of 3 characters is also intentional — single and double character queries return extremely broad, low-quality results. For production, consider adding a proximity parameter with the user's current location bias so that "Main St" returns the nearest Main Street rather than the most globally prominent one. You can get coordinates from the browser's navigator.geolocation API and pass them as proximity=${lng},${lat} in the query string.
'use client';
import { useState, useEffect, useRef } from 'react';
import { geocode, GeocodingResult } from '@/lib/mapbox';
export function AddressSearch({ onSelect }: {
onSelect: (result: GeocodingResult) => void;
}) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<GeocodingResult[]>([]);
const [isOpen, setIsOpen] = useState(false);
const debounceRef = useRef<NodeJS.Timeout>();
useEffect(() => {
if (query.length < 3) {
setResults([]);
return;
}
clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(async () => {
const hits = await geocode(query);
setResults(hits);
setIsOpen(true);
}, 300);
return () => clearTimeout(debounceRef.current);
}, [query]);
return (
<div className="relative">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search for an address..."
className="w-full p-3 border rounded"
/>
{isOpen && results.length > 0 && (
<ul className="absolute z-10 w-full bg-white border rounded shadow-lg mt-1">
{results.map((result, i) => (
<li
key={i}
onClick={() => {
onSelect(result);
setQuery(result.placeName);
setIsOpen(false);
}}
className="p-3 hover:bg-gray-100 cursor-pointer"
>
{result.placeName}
</li>
))}
</ul>
)}
</div>
);
}
5. Map Integration
The mapbox-gl library is the official Mapbox renderer and gives you the full feature set: vector tiles, custom styles, 3D terrain, and smooth animations like flyTo. The main alternative is MapLibre GL JS, an open-source fork of the pre-commercialization Mapbox GL that does not require a Mapbox access token for rendering — useful if you're serving your own tiles. For most teams using Mapbox-hosted tiles and geocoding, mapbox-gl is the right choice since it stays in sync with Mapbox's latest style spec. If bundle size is a concern, mapbox-gl is around 250KB gzipped; consider lazy-loading the component so it doesn't block initial page render.
'use client';
import { useEffect, useRef, useState } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { AddressSearch } from './AddressSearch';
import { GeocodingResult } from '@/lib/mapbox';
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN!;
export function GeocodingMap() {
const mapContainer = useRef<HTMLDivElement>(null);
const map = useRef<mapboxgl.Map | null>(null);
const marker = useRef<mapboxgl.Marker | null>(null);
useEffect(() => {
if (!mapContainer.current) return;
map.current = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/streets-v12',
center: [-74.006, 40.7128],
zoom: 12,
});
return () => map.current?.remove();
}, []);
const handleSelect = (result: GeocodingResult) => {
if (!map.current) return;
const [lng, lat] = result.coordinates;
// Remove old marker
marker.current?.remove();
// Add new marker
marker.current = new mapboxgl.Marker()
.setLngLat([lng, lat])
.setPopup(new mapboxgl.Popup().setHTML(`<h3>${result.placeName}</h3>`))
.addTo(map.current);
// Fly to location
map.current.flyTo({
center: [lng, lat],
zoom: 14,
duration: 1500,
});
};
return (
<div>
<AddressSearch onSelect={handleSelect} />
<div ref={mapContainer} style={{ height: '500px', marginTop: '16px' }} />
</div>
);
}
6. Batch Geocoding
Batch geocoding comes up in two situations: importing a CSV of locations that were entered without coordinates (think: a customer uploaded their store list as a spreadsheet), and data enrichment pipelines where you need to append coordinates to existing records in a database. Mapbox does not have a dedicated batch endpoint — every geocode is a separate HTTP request — so the implementation is about managing parallelism responsibly. Running all requests in parallel will hit the 600 requests/minute rate limit quickly. The pattern below processes in chunks of 10 with a 1-second pause between chunks, which keeps throughput at roughly 300 addresses per minute — fast enough for most import jobs without triggering 429 errors.
export async function batchGeocode(addresses: string[]): Promise<GeocodingResult[]> {
const results: GeocodingResult[] = [];
// Mapbox doesn't have a batch API — process in parallel with rate limiting
const BATCH_SIZE = 10;
for (let i = 0; i < addresses.length; i += BATCH_SIZE) {
const batch = addresses.slice(i, i + BATCH_SIZE);
const promises = batch.map(addr => geocode(addr));
const batchResults = await Promise.all(promises);
for (const result of batchResults) {
if (result.length > 0) {
results.push(result[0]); // Take first result
}
}
// Rate limit: 600 requests/minute
if (i + BATCH_SIZE < addresses.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return results;
}
Pricing
| Feature | Free Tier | Paid |
|---|---|---|
| Geocoding | 100,000 requests/month | $0.75/1,000 after free tier |
| Map loads | 50,000/month | $5.00/1,000 after free tier |
| Directions | 100,000 requests/month | $0.50/1,000 |
Compared to Google Maps Geocoding, Mapbox is significantly cheaper at scale. Google's free tier is 40,000 requests/month (versus Mapbox's 100,000), and paid usage runs $5.00/1,000 requests — about 6.7x more expensive than Mapbox's $0.75. For a high-traffic store finder making 500,000 geocoding requests per month, Google would cost roughly $2,300 versus Mapbox's $300. The cost difference narrows if you're already paying for Google Maps Platform and can negotiate volume pricing, but for greenfield projects the math strongly favors Mapbox. See our full Google Maps vs Mapbox comparison for a breakdown of accuracy, coverage, and developer experience differences beyond pricing.
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Not debouncing autocomplete | Excessive API calls | Debounce by 300ms |
| Using coordinates as [lat, lng] | Map shows wrong location | Mapbox uses [lng, lat] order |
| Not handling no-results | UI shows empty state | Show "No results found" message |
| Exposing token without restrictions | Quota abuse | Restrict token to your domain in Mapbox dashboard |
| Client-side batch geocoding | Slow, rate limited | Use server-side with controlled rate |
Handling Geocoding Errors Gracefully
Geocoding failures come in two forms that require different handling strategies. HTTP-level errors (401 unauthorized, 429 rate limited, 503 service unavailable) indicate problems with the request or infrastructure — these should be caught at the fetch layer and surfaced as distinct error states so the UI can respond appropriately. Data-level failures occur when the API returns HTTP 200 but features is an empty array, which happens when the query produces no matches, not when something went wrong technically.
For autocomplete, the empty array case is normal user behavior — partial input often produces no results — so the correct response is to hide the dropdown, not show an error message. For geocoding a specific address during data import or form submission, an empty result means the address could not be verified and warrants user feedback asking them to check the input.
Rate limit handling is particularly important for batch workflows. Mapbox returns 429 Too Many Requests when you exceed 600 requests per minute. Implement exponential backoff starting at 1 second and capping at 30 seconds. For client-side autocomplete, add a minimum character threshold (3+) and debounce (300ms) as rate-limiting strategies before you hit API limits.
export async function geocodeSafe(query: string): Promise<GeocodingResult[]> {
try {
const res = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(query)}.json?access_token=${MAPBOX_TOKEN}&limit=5`
);
if (res.status === 429) throw new Error('rate_limited');
if (!res.ok) throw new Error(`api_error_${res.status}`);
const data = await res.json();
return data.features.map((f: any) => ({
placeName: f.place_name,
coordinates: f.center as [number, number],
type: f.place_type[0],
}));
} catch (err) {
console.error('Geocoding failed:', err);
return []; // Fail gracefully — never crash the UI
}
}
Geocoding Accuracy and Coverage Considerations
Mapbox Geocoding covers 195+ countries but accuracy varies significantly by region and address type. In the United States, Canada, Western Europe, and Australia, address-level geocoding is highly accurate — street-number precision for the majority of valid addresses. In emerging markets, coverage thins at the street level; many rural addresses resolve to neighborhood or city centroids rather than precise coordinates. For product decisions, validate geocoding in your actual target markets before launch — a delivery app for São Paulo faces different accuracy conditions than one for London.
The place_type field in the response tells you how specific the result is. address means a precise street address was matched. postcode means only the postal code was resolved. place means a city or town. If your use case requires precise address matching (delivery apps, store finders), filter results by place_type === 'address' and surface a "no precise match found" message for lower-precision results rather than silently accepting an imprecise coordinate.
Point-of-interest (POI) data freshness is a separate concern from address coverage. New businesses, closed businesses, and recently renamed locations may be stale by weeks to months. For use cases where POI accuracy is critical — business directories, store finders, local search — cross-reference against a specialized data source rather than relying on geocoding alone. The Mapbox Permanent Geocoding endpoint (which allows caching, unlike the standard endpoint) covers stored bulk results without the no-persistence restriction.
Want a deeper comparison before you commit to Mapbox? Read our Google Maps vs Mapbox API guide for a full breakdown of accuracy, coverage, and pricing at scale. For a broader look at the location data space, see our best maps and location APIs roundup. And if you're evaluating APIs more generally, our guide to evaluating an API before committing walks through the due-diligence checklist.