How to Add Geocoding to Your App with Mapbox
·APIScout Team
mapboxgeocodingmapstutorialapi integration
How to Add Geocoding to Your App with Mapbox
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.
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).
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
// 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
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
'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
'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
For geocoding many addresses at once (server-side):
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 |
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 |
Building with maps? Compare Mapbox vs Google Maps vs HERE on APIScout — pricing, accuracy, and customization options.