DocsBatch Geocoding

Batch Geocoding

At ~100 ms per request, geocoding 10,000 addresses sequentially takes over 15 minutes. Running 20 requests in parallel cuts that to about a minute. Each example below uses the idiom most natural to its language:

LanguageConcurrency mechanism
GoBuffered channel as semaphore + goroutines + sync.WaitGroup
JavaScriptPromise.allSettled over chunks of 20
Pythonasyncio.Semaphore(20) + asyncio.gather
Ruststream::iter().buffer_unordered(20)
// batch_geocode.go
// go mod init yourapp && go mod tidy
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"sync"
)

const concurrency = 20

type GeoResult struct {
	Lat         string `json:"lat"`
	Lon         string `json:"lon"`
	DisplayName string `json:"display_name"`
}

func geocode(client *http.Client, address string) (*GeoResult, error) {
	u := "https://api.pickpoint.io/v2/geocode/forward?limit=1&q=" + url.QueryEscape(address)
	req, _ := http.NewRequest("GET", u, nil)
	req.Header.Set("X-Api-Key", os.Getenv("PICKPOINT_KEY"))

	resp, err := client.Do(req)
	if err != nil { return nil, err }
	defer resp.Body.Close()

	var results []GeoResult
	json.NewDecoder(resp.Body).Decode(&results)
	if len(results) == 0 { return nil, fmt.Errorf("no result") }
	return &results[0], nil
}

func batchGeocode(addresses []string) []*GeoResult {
	// Buffered channel works as a semaphore: at most 20 goroutines run at once
	sem := make(chan struct{}, concurrency)
	out := make([]*GeoResult, len(addresses))
	var mu sync.Mutex
	var wg sync.WaitGroup
	client := &http.Client{}

	for i, addr := range addresses {
		wg.Add(1)
		go func(i int, addr string) {
			defer wg.Done()
			sem <- struct{}{}        // acquire
			defer func() { <-sem }() // release

			geo, err := geocode(client, addr)
			if err != nil {
				fmt.Fprintf(os.Stderr, "[%d] error: %v\n", i, err)
				return
			}
			mu.Lock()
			out[i] = geo
			mu.Unlock()
		}(i, addr)
	}

	wg.Wait()
	return out
}

func main() {
	addresses := []string{"Eiffel Tower, Paris", "Big Ben, London", "Colosseum, Rome"}
	for i, g := range batchGeocode(addresses) {
		if g != nil { fmt.Printf("[%d] %s, %s\n", i, g.Lat, g.Lon) }
	}
}
// batch-geocode.mjs  (Node.js 18+)
import { writeFileSync } from 'fs';

const API_KEY     = process.env.PICKPOINT_KEY;
const CONCURRENCY = 20;

async function geocode(address) {
  const res = await fetch(
    `https://api.pickpoint.io/v2/geocode/forward?q=${encodeURIComponent(address)}&limit=1`,
    { headers: { 'X-Api-Key': API_KEY } }
  );
  const data = await res.json();
  return data[0] ?? null;
}

async function batchGeocode(addresses) {
  const results = new Array(addresses.length).fill(null);

  // Process in chunks of CONCURRENCY; within each chunk all run in parallel
  for (let i = 0; i < addresses.length; i += CONCURRENCY) {
    const chunk   = addresses.slice(i, i + CONCURRENCY);
    const settled = await Promise.allSettled(chunk.map(geocode));

    settled.forEach((s, j) => {
      if (s.status === 'fulfilled') results[i + j] = s.value;
      else console.error(`[error] ${chunk[j]}: ${s.reason}`);
    });

    process.stdout.write(`\r${Math.min(i + CONCURRENCY, addresses.length)}/${addresses.length}`);
  }

  console.log('\nDone.');
  return results;
}

// ── main ──
const addresses = ['Eiffel Tower, Paris', 'Big Ben, London', 'Colosseum, Rome'];
const geos = await batchGeocode(addresses);
writeFileSync('geocoded.json', JSON.stringify(
  addresses.map((a, i) => ({ address: a, result: geos[i] })), null, 2
));
# batch_geocode.py
# pip install httpx
import asyncio, json, os, sys
import httpx

API_KEY     = os.environ["PICKPOINT_KEY"]
CONCURRENCY = 20

async def geocode(client: httpx.AsyncClient, sem: asyncio.Semaphore,
                  address: str) -> dict | None:
    async with sem:
        r = await client.get(
            "https://api.pickpoint.io/v2/geocode/forward",
            params={"q": address, "limit": 1},
            headers={"X-Api-Key": API_KEY},
        )
        data = r.json()
        return data[0] if data else None

async def batch_geocode(addresses: list[str]) -> list[dict | None]:
    sem = asyncio.Semaphore(CONCURRENCY)
    async with httpx.AsyncClient(timeout=15) as client:
        tasks = [geocode(client, sem, a) for a in addresses]
        return await asyncio.gather(*tasks, return_exceptions=True)

# ── main ──
if __name__ == "__main__":
    addresses = ["Eiffel Tower, Paris", "Big Ben, London", "Colosseum, Rome"]
    results = asyncio.run(batch_geocode(addresses))
    print(json.dumps([{"address": a, "result": r}
                       for a, r in zip(addresses, results)], indent=2))
// Cargo.toml:
//   [dependencies]
//   tokio   = { version = "1", features = ["full"] }
//   reqwest = { version = "0.12", features = ["json"] }
//   futures = "0.3"
//   serde   = { version = "1", features = ["derive"] }

use futures::stream::{self, StreamExt};
use reqwest::Client;
use serde::Deserialize;
use std::env;

const CONCURRENCY: usize = 20;

#[derive(Debug, Deserialize)]
struct GeoResult { lat: String, lon: String, display_name: String }

async fn geocode(client: &Client, address: String) -> Option<GeoResult> {
    let resp = client
        .get("https://api.pickpoint.io/v2/geocode/forward")
        .query(&[("q", &address), ("limit", &"1".to_string())])
        .header("X-Api-Key", env::var("PICKPOINT_KEY").unwrap())
        .send().await.ok()?;

    resp.json::<Vec<GeoResult>>().await.ok()?.into_iter().next()
}

#[tokio::main]
async fn main() {
    let addresses = vec!["Eiffel Tower, Paris", "Big Ben, London", "Colosseum, Rome"];
    let client    = Client::new();

    // buffer_unordered(20) keeps exactly 20 futures running at all times
    let results: Vec<_> = stream::iter(addresses.iter())
        .map(|&addr| geocode(&client, addr.to_string()))
        .buffer_unordered(CONCURRENCY)
        .collect()
        .await;

    for (addr, geo) in addresses.iter().zip(&results) {
        match geo {
            Some(g) => println!("{}{}, {}", addr, g.lat, g.lon),
            None    => eprintln!("no result: {}", addr),
        }
    }
}

Throughput

With 20 concurrent workers at ~100 ms average latency: ~200 req/sec, ~720k/hour.

AddressesEstimated time
1,000~5 seconds
10,000~1 minute
100,000~8–10 minutes
1,000,000~90 minutes

Tips

  • Cache. Store coordinates in a database - geocoding output for a given address is stable. Never re-geocode the same address twice.
  • Clean addresses first. Add city and country when missing. Vague queries like "Main Street" return the wrong place.
  • Handle null results. Some addresses won't match. Log them for manual review or fall back to city-level geocoding.
  • Back off on 429. If the API returns 429 Too Many Requests, wait and retry with exponential back-off.
  • Add countrycodes. Pass countrycodes=us (or equivalent) if all addresses are in one country - fewer false matches.

Geocoding guideBackend Examples