Skip to main content

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

  1. Sign up at mapbox.com
  2. Go to Account → Access Tokens
  3. 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';
}
'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

FeatureFree TierPaid
Geocoding100,000 requests/month$0.75/1,000 after free tier
Map loads50,000/month$5.00/1,000 after free tier
Directions100,000 requests/month$0.50/1,000

Common Mistakes

MistakeImpactFix
Not debouncing autocompleteExcessive API callsDebounce by 300ms
Using coordinates as [lat, lng]Map shows wrong locationMapbox uses [lng, lat] order
Not handling no-resultsUI shows empty stateShow "No results found" message
Exposing token without restrictionsQuota abuseRestrict token to your domain in Mapbox dashboard
Client-side batch geocodingSlow, rate limitedUse server-side with controlled rate

Building with maps? Compare Mapbox vs Google Maps vs HERE on APIScout — pricing, accuracy, and customization options.

Comments