How to Geocode a List of Addresses

API or service
Batch Geocoding + Batch Requests APIs
Task
Address list → ordered geocoding results
Examples
URLJavaScriptPython
Difficulty
Intermediate
Time
15 min

You have a list of addresses—for example, rows from a spreadsheet, customer records, or delivery stops—and need coordinates and structured address fields for every item. The result must remain associated with the original record, including when an address is unmatched or a request fails.

The Geoapify Batch Geocoding API can process multiple free-form addresses in one job. For structured inputs or workflows that need more control, use the Generic Batch Requests API or call the Forward Geocoding API one address at a time.

Bulk processing must account for request-rate limits, account quotas, asynchronous jobs, partial failures, retries, and result ordering. Give every input a stable identifier and store it with the result so processing can be resumed safely. Rate limits control how quickly requests may be sent, while daily or monthly limits control total usage. Both depend on the active project, so check the limits in Geoapify MyProjects instead of hard-coding plan values.

Choose a bulk-geocoding approach

Input and requirement Recommended approach
Up to 1,000 free-form address strings Batch Geocoding API
Up to 1,000 structured addresses Generic Batch Requests API
Call the API one address at a time while respecting rate limits Forward Geocoding API, using @geoapify/request-rate-limiter in JavaScript or pacing with retries and checkpoints in Python

Choose a batch API when results can be processed as a job. Call the API one address at a time when the application needs an immediate result for each item, custom retry behavior, or checkpoints between requests.

Batch endpoints return 200 when work completes immediately or 202 with a job ID while processing continues.

Batch geocode free-form addresses

Use the Batch Geocoding API when each input is a complete address string. One batch can contain from 1 to 1,000 addresses. Creating the task may return the completed results immediately with HTTP 200, but larger batches normally return HTTP 202 with a job ID that you poll until the result endpoint returns 200.

API requests

Create a batch task

Send the address strings as a JSON array. Shared options such as lang, filter, and bias can be added as query parameters.

curl --request POST \
  "https://api.geoapify.com/v1/batch/geocode/search?apiKey=YOUR_API_KEY" \
  --header "Content-Type: application/json" \
  --data '[
    "901 South Orange Blossom Trail, Orlando, FL 32805, USA",
    "2015 South Orange Blossom Trail, Orlando, FL 32805, USA"
  ]'

An unfinished task returns HTTP 202. Save its id; the returned url points to the same result endpoint.

{
  "id": "e426d306e1194a05a61561825b99a922",
  "status": "pending",
  "url": "https://api.geoapify.com/v1/batch/geocode/search?id=e426d306e1194a05a61561825b99a922&apiKey=YOUR_API_KEY"
}

Get the results

Send a GET request with the task ID. Repeat this request while the response status is 202; the results are ready when it returns 200.

curl "https://api.geoapify.com/v1/batch/geocode/search?id=e426d306e1194a05a61561825b99a922&format=json&apiKey=YOUR_API_KEY"

Reduced response: The completed response contains one normalized geocoding result for each input address, in the same order as the submitted strings.

[
  {
    "query": {
      "text": "901 South Orange Blossom Trail, Orlando, FL 32805, USA"
    },
    "housenumber": "901",
    "street": "South Orange Blossom Trail",
    "city": "Orlando",
    "state": "Florida",
    "state_code": "FL",
    "postcode": "32805",
    "country": "United States",
    "country_code": "us",
    "formatted": "901 South Orange Blossom Trail, Orlando, FL 32805, United States of America",
    "lat": 28.530995,
    "lon": -81.397028,
    "result_type": "building"
  },
  {
    "query": {
      "text": "2015 South Orange Blossom Trail, Orlando, FL 32805, USA"
    },
    "housenumber": "2015",
    "street": "South Orange Blossom Trail",
    "city": "Holden Heights",
    "state": "Florida",
    "state_code": "FL",
    "postcode": "32805",
    "country": "United States",
    "country_code": "us",
    "formatted": "2015 South Orange Blossom Trail, Holden Heights, FL 32805, United States of America",
    "lat": 28.5207189,
    "lon": -81.3969743,
    "result_type": "building"
  }
]

Batch geocode structured addresses

Use the Generic Batch Requests API when address components are already stored in separate fields. Put parameters shared by every address at the top level and address-specific fields inside each input. A task can contain from 1 to 1,000 inputs.

API requests

Create a batch task

Set api to /v1/geocode/search. Give each input a stable id so the completed result can be matched to the original record without relying only on array position.

curl --request POST \
  "https://api.geoapify.com/v1/batch?apiKey=YOUR_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "api": "/v1/geocode/search",
    "params": { "limit": 1 },
    "inputs": [
      {
        "id": "museum",
        "params": {
          "housenumber": "200",
          "street": "Larkin Street",
          "postcode": "94102",
          "city": "San Francisco",
          "country": "United States"
        }
      },
      {
        "id": "library",
        "params": {
          "housenumber": "100",
          "street": "Larkin Street",
          "postcode": "94102",
          "city": "San Francisco",
          "country": "United States"
        }
      }
    ]
  }'

An unfinished task returns HTTP 202 with the ID required to retrieve it. The default priority is 0.5; set a value from 0.5 to 1 when the processing speed and credit-cost tradeoff needs to be adjusted.

{
  "id": "884f79702109455199c6c60cf7201207",
  "status": "pending"
}

Get the results

Poll the task by ID. A 202 response means it is still processing; repeat the request until it returns 200.

curl "https://api.geoapify.com/v1/batch?id=884f79702109455199c6c60cf7201207&apiKey=YOUR_API_KEY"

Reduced response: Each item repeats its input id and wraps the Forward Geocoding API response in result.

{
  "api": "/v1/geocode/search",
  "params": { "limit": 1 },
  "id": "884f79702109455199c6c60cf7201207",
  "results": [
    {
      "id": "museum",
      "result": {
        "type": "FeatureCollection",
        "features": [
          {
            "type": "Feature",
            "properties": {
              "name": "Asian Art Museum of San Francisco",
              "housenumber": "200",
              "street": "Larkin Street",
              "city": "San Francisco",
              "state": "California",
              "state_code": "CA",
              "postcode": "94102",
              "country": "United States",
              "country_code": "us",
              "formatted": "Asian Art Museum of San Francisco, 200 Larkin Street, San Francisco, CA 94102, United States of America",
              "lat": 37.780315,
              "lon": -122.4159922,
              "result_type": "amenity"
            },
            "geometry": {
              "type": "Point",
              "coordinates": [-122.4159922, 37.780315]
            }
          }
        ]
      }
    },
    {
      "id": "library",
      "result": {
        "type": "FeatureCollection",
        "features": [
          {
            "type": "Feature",
            "properties": {
              "name": "San Francisco Public Library",
              "housenumber": "100",
              "street": "Larkin Street",
              "city": "San Francisco",
              "state": "California",
              "state_code": "CA",
              "postcode": "94102",
              "country": "United States",
              "country_code": "us",
              "formatted": "San Francisco Public Library, 100 Larkin Street, San Francisco, CA 94102, United States of America",
              "lat": 37.7790262,
              "lon": -122.415807,
              "result_type": "amenity"
            },
            "geometry": {
              "type": "Point",
              "coordinates": [-122.415807, 37.7790262]
            }
          }
        ]
      }
    }
  ]
}

Create and poll a batch with JavaScript

This example uses the Batch Geocoding API for free-form addresses. It creates a task with fetch(), saves the returned job ID, and polls until the API returns the completed results with HTTP 200.

const apiKey = "YOUR_API_KEY";
const endpoint = "https://api.geoapify.com/v1/batch/geocode/search";
const pollIntervalMs = 5000;

// Each string becomes one item in the completed batch response.
const addresses = [
  "901 South Orange Blossom Trail, Orlando, FL 32805, USA",
  "2015 South Orange Blossom Trail, Orlando, FL 32805, USA"
];

const wait = milliseconds =>
  new Promise(resolve => setTimeout(resolve, milliseconds));

async function readBatch(response) {
  // A batch is either complete (200) or still processing (202).
  if (response.status !== 200 && response.status !== 202) {
    throw new Error(`Batch request failed: ${response.status}`);
  }

  const data = await response.json();
  if (response.status === 200) return data;

  // Keep the task ID returned by the initial POST request.
  const jobId = data.id;

  while (true) {
    // Avoid sending continuous status requests while the task is running.
    await wait(pollIntervalMs);

    response = await fetch(
      `${endpoint}?id=${encodeURIComponent(jobId)}&format=json&apiKey=${encodeURIComponent(apiKey)}`
    );

    if (response.status !== 200 && response.status !== 202) {
      throw new Error(`Batch result request failed: ${response.status}`);
    }

    const result = await response.json();
    // A 202 response starts another iteration; 200 returns the results.
    if (response.status === 200) return result;
  }
}

// Create the batch task by sending all free-form addresses in one request.
const response = await fetch(`${endpoint}?apiKey=${encodeURIComponent(apiKey)}`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(addresses)
});

// Return immediately for a completed task or poll a pending one.
const results = await readBatch(response);
console.log(results);

The function also handles the less common case where task creation completes immediately with 200. For large jobs, use a polling interval of up to 60 seconds. Add an overall timeout or maximum attempt count in production so the application cannot poll forever.

The same polling pattern works with the Generic Batch Requests API. Use /v1/batch as the endpoint and submit the structured batch object shown in the previous example.

Create and poll a batch with Python

This example uses the third-party requests package with the Batch Geocoding API. It submits free-form addresses, retains the returned job ID, and requests the result until the API responds with HTTP 200.

import time
import requests

API_KEY = "YOUR_API_KEY"
ENDPOINT = "https://api.geoapify.com/v1/batch/geocode/search"
POLL_INTERVAL_SECONDS = 5

# Each string becomes one item in the completed batch response.
addresses = [
    "901 South Orange Blossom Trail, Orlando, FL 32805, USA",
    "2015 South Orange Blossom Trail, Orlando, FL 32805, USA",
]

# Create the batch task by sending all free-form addresses in one request.
response = requests.post(
    ENDPOINT,
    params={"apiKey": API_KEY},
    json=addresses,
    timeout=30,
)
response.raise_for_status()

# A 202 response means the task must be polled by its returned ID.
if response.status_code == 202:
    job_id = response.json()["id"]

    while response.status_code == 202:
        # Wait between checks instead of continuously requesting the status.
        time.sleep(POLL_INTERVAL_SECONDS)
        response = requests.get(
            ENDPOINT,
            params={"id": job_id, "format": "json", "apiKey": API_KEY},
            timeout=30,
        )
        response.raise_for_status()

# At this point, both immediate and polled responses have HTTP status 200.
results = response.json()
print(results)

raise_for_status() stops on unsuccessful responses while allowing both 200 and 202. For large jobs, use a polling interval of up to 60 seconds. Add a maximum wait time and persist the job ID when a production process must resume after an interruption.

The same lifecycle works with the Generic Batch Requests API. Change the endpoint to /v1/batch and submit the structured batch object shown in the previous example.

Rate-limit individual geocoding requests with JavaScript

Use @geoapify/request-rate-limiter when every address needs an individual synchronous Geocoding API request.

npm install @geoapify/request-rate-limiter
import RequestRateLimiter from "@geoapify/request-rate-limiter";

const apiKey = "YOUR_API_KEY";
const addresses = ["Alexanderplatz, Berlin", "10 Champs-Élysées, Paris"];

// Create request functions without starting the HTTP requests yet.
const requests = addresses.map(address => async () => {
  const params = new URLSearchParams({
    text: address,
    format: "json",
    limit: "1",
    apiKey
  });

  const response = await fetch(
    `https://api.geoapify.com/v1/geocode/search?${params}`
  );
  if (!response.ok) throw new Error(`Request failed: ${response.status}`);
  return response.json();
});

// Start at most 5 requests per second and keep concurrency at 2.
const results = await RequestRateLimiter.rateLimitedRequests(
  requests,
  5,
  1000,
  { maxConcurrentRequests: 2 }
);

console.log(results);

Pass functions that start requests—not promises that have already started. Set the rate and concurrency values to the limits of the active project.

Pace individual geocoding requests with Python

Use this approach when addresses come from a text file and each one should be sent to the Forward Geocoding API as an individual request. The script reads one address per line, submits a limited group of requests each second, preserves the input order, and writes one JSON object per line to the output file.

The example uses itertools.batched(), available in Python 3.12 and later, and the third-party requests package.

import itertools as it
import json
import os
from concurrent.futures import ALL_COMPLETED, ThreadPoolExecutor, wait
from time import sleep

import requests

GEOCODING_URL = "https://api.geoapify.com/v1/geocode/search"
REQUESTS_PER_SECOND = 5
MAX_WORKERS = 10
MAX_RETRIES = 3


def geocode_address(address, api_key, country_code):
    # Build one Forward Geocoding API request for this input line.
    params = {
        "text": address,
        "filter": f"countrycode:{country_code.lower()}",
        "format": "json",
        "limit": 1,
        "apiKey": api_key,
    }

    for attempt in range(MAX_RETRIES):
        try:
            response = requests.get(GEOCODING_URL, params=params, timeout=30)

            # Follow the server-provided delay before retrying a rate-limited call.
            if response.status_code == 429:
                retry_after = float(response.headers.get("Retry-After", "1"))
                sleep(retry_after)
                continue

            response.raise_for_status()
            data = response.json()

            # Keep the original address beside its first match, or null if unmatched.
            result = data["results"][0] if data["results"] else None
            return {"address": address, "result": result}

        except (requests.RequestException, ValueError) as error:
            if attempt == MAX_RETRIES - 1:
                return {"address": address, "error": str(error)}

            # Back off before retrying network, HTTP, or response-decoding failures.
            sleep(2 ** attempt)

    return {"address": address, "error": "Rate limit retry count exceeded"}


def geocode_addresses(api_key, input_file, output_file, country_code):
    # Treat every non-empty input line as one address.
    with open(input_file, "r", encoding="utf-8") as file:
        addresses = [line.strip() for line in file if line.strip()]

    # Submit no more than REQUESTS_PER_SECOND new tasks in each interval.
    address_batches = it.batched(addresses, REQUESTS_PER_SECOND)
    tasks = []

    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        for batch in address_batches:
            tasks.extend([
                executor.submit(
                    geocode_address,
                    address,
                    api_key,
                    country_code,
                )
                for address in batch
            ])
            sleep(1)

    # task.result() follows submission order even if requests finish out of order.
    wait(tasks, return_when=ALL_COMPLETED)
    results = [task.result() for task in tasks]

    # Write one independently readable result or error record per line.
    with open(output_file, "w", encoding="utf-8") as file:
        for result in results:
            file.write(json.dumps(result, ensure_ascii=False) + "\n")


# Keep the API key outside the source file.
geocode_addresses(
    api_key=os.environ["GEOAPIFY_API_KEY"],
    input_file="addresses.txt",
    output_file="results.jsonl",
    country_code="us",
)

REQUESTS_PER_SECOND controls how many tasks are submitted in each one-second interval. Set it to the request-rate limit of the active Geoapify project; do not assume that the example value applies to every plan. MAX_WORKERS limits concurrent network calls, while MAX_RETRIES prevents an address from being retried forever.

The countrycode filter limits results to the supplied two-letter country code. Remove the filter when an input file can contain addresses from multiple countries.

The output uses JSON Lines, so each source address has its own result or error record. Because tasks retains submission order, the output lines follow the input lines even when requests finish in a different order. For a long-running import, write completed groups incrementally or record checkpoints so an interrupted job can resume without repeating the entire file.