Go Geocoding Tutorial: Goroutines, Bounded Concurrency и Backoff
Production Go tutorial за geocoding: goroutine pools, semaphore-bounded concurrency, exponential backoff на 429. Компилира и работи.
Go е езикът, към който посягаш, когато искаш concurrency без церемонията на async/await. Goroutines са stack-ове по 2 KB, channels са типизирани pipes, а стандартната библиотека net/http е достатъчно добра за production от първия ден. Няма event loop за учене, няма ключова дума await за разпиляване из всяка функция и няма проблем с coloured-function. Пишеш праволинеен код и го разпаралелваш с go и semaphore channel.
Това е работещ Go tutorial за geocoding на адреси през REST API. Всеки snippet е писан в .go файл, прекаран през go build и изпълнен срещу живия API на чиста Go 1.22 инсталация преди публикуване. Няма SDK, няма third-party HTTP библиотека, няма framework — само net/http, encoding/json, encoding/csv и context. Накрая ще имаш програма, която стриймва CSV от всякакъв размер, геокодира редовете му с конфигурируем goroutine pool, ретраи 429 и 5xx с jittered exponential backoff, който уважава Retry-After, и пише {lat, lng, error} CSV без никога да държи входния файл в паметта. Около 80 реда Go.
Endpoint-ът, използван навсякъде, е csv2geo.com/api/v1. Free tier-ът е 1000 forward/reverse заявки на ден плюс 100 batch реда на ден, без кредитна карта. Влез и вземи ключ от /api-keys, за да следваш статията.
Endpoint-ът
Два endpoint-а покриват почти всяка реална geocoding задача. Forward превръща string-адрес в координати. Reverse превръща координати в адрес. Двата приемат GET (single) или POST (batch).
# Forward (single)
GET https://csv2geo.com/api/v1/geocode?q=ADDRESS&country=US
# Reverse (single)
GET https://csv2geo.com/api/v1/reverse?lat=LAT&lng=LNG
# Batch forward
POST https://csv2geo.com/api/v1/geocode
Body: { "addresses": ["addr1", "addr2", ...] }
# Auth: either ?api_key=KEY query string, or
# Authorization: Bearer KEY headerФормата на отговора за единична forward заявка изглежда така. Това е реален output, не пример от документацията.
{
"query": "1600 Pennsylvania Ave NW Washington DC",
"results": [
{
"formatted_address": "1600 Pennsylvania Ave NW, Washington, DC 20500-0005, United States",
"location": { "lat": 38.89768, "lng": -77.03655 },
"accuracy": "houseNumber",
"accuracy_score": 1,
"components": {
"house_number": "1600",
"street": "Pennsylvania Ave NW",
"city": "Washington",
"state": "District of Columbia",
"postal_code": "20500-0005",
"country": "USA"
}
}
],
"meta": { "response_time_ms": 673, "source": "here" }
}Две полета имат най-голямо значение. results[0].location е твоят {lat, lng}. results[0].accuracy е нивото на match-а — "houseNumber" е rooftop, "street" е центроид на улицата, "place" е POI match, "postcode" е центроид на postcode-а. Числовият accuracy_score (0.0–1.0) дава по-фин threshold; виж geocoding confidence scores explained как да избереш.
Първа заявка
net/http плюс encoding/json е всичко, което ти трябва. Първата версия връща само координатите, за да можем да проверим wire format-а от край до край.
// geocode.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"time"
)
const baseURL = "https://csv2geo.com/api/v1"
type Location struct {
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
}
type GeocodeResult struct {
FormattedAddress string `json:"formatted_address"`
Location Location `json:"location"`
Accuracy string `json:"accuracy"`
AccuracyScore float64 `json:"accuracy_score"`
}
type GeocodeResponse struct {
Query string `json:"query"`
Results []GeocodeResult `json:"results"`
}
var client = &http.Client{Timeout: 10 * time.Second}
func geocode(address, country string) (*Location, error) {
q := url.Values{}
q.Set("q", address)
if country != "" {
q.Set("country", country)
}
req, err := http.NewRequest("GET", baseURL+"/geocode?"+q.Encode(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("CSV2GEO_KEY"))
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return nil, fmt.Errorf("http %d", resp.StatusCode)
}
var out GeocodeResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
}
if len(out.Results) == 0 {
return nil, nil
}
return &out.Results[0].Location, nil
}
func main() {
loc, err := geocode("1 Apple Park Way, Cupertino, CA", "US")
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Printf("%+v\n", loc)
}Изпълни:
CSV2GEO_KEY=geo_live_xxx go run geocode.go
# &{Lat:37.33177 Lng:-122.03042}Няколко неща си струва да отбележим. defer resp.Body.Close() не е опционално — ако забравиш, лежащото отдолу TCP-съединение не може да се върне в pool-а и под натиск ще изтечеш file descriptor-и. Използвай един package-level http.Client вместо http.Get директно; default клиентът има Timeout: 0, което значи, че замръзнал сървър може да блокира твоята goroutine завинаги. Използвай Authorization: Bearer вместо query формата ?api_key=, за да не попадат ключовете в shell history или log файлове.
Reverse geocoding
Същата форма, различни параметри. Подай lat и lng, получи в отговор адрес.
type ReverseResult struct {
FormattedAddress string `json:"formatted_address"`
Location Location `json:"location"`
Accuracy string `json:"accuracy"`
DistanceMeters float64 `json:"distance_meters,omitempty"`
}
type ReverseResponse struct {
Results []ReverseResult `json:"results"`
}
func reverse(lat, lng float64) (*ReverseResult, error) {
q := url.Values{}
q.Set("lat", fmt.Sprintf("%f", lat))
q.Set("lng", fmt.Sprintf("%f", lng))
req, err := http.NewRequest("GET", baseURL+"/reverse?"+q.Encode(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("CSV2GEO_KEY"))
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return nil, fmt.Errorf("http %d", resp.StatusCode)
}
var out ReverseResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
}
if len(out.Results) == 0 {
return nil, nil
}
return &out.Results[0], nil
}Reverse отговорите включват поле distance_meters — колко далеч matched адресът сяда от входната координата. Ако правиш reverse-geocoding на GPS пинговете на delivery приложение, всичко над ~50м обикновено значи, че GPS fix-ът е бил лош, не геокодерът. Под ~10м гледаш rooftop. Ground-truth distance е единствената честна метрика за accuracy при reverse geocoding; всичко друго лъже малко.
Bounded concurrency по Go-шкия начин
Грешният начин да геокодираш 10 000 адреса в Go е for _, a := range addrs { go geocode(a) }. Това запалва 10 000 unbounded goroutines, отваря 10 000 TCP съединения, изчерпва rate limit-а на API на втората итерация и изчерпва локалните ти file descriptor-и в същото време. Goroutines са евтини, но не са безплатни, а мрежата никога не е безплатна.
Правилният начин е bounded concurrency със semaphore channel. Buffered channel от struct{} с капацитет N е каноничният Go семафор — изпращането в него блокира, щом N goroutines вече са in-flight.
// pool.go
package main
import (
"fmt"
"sync"
)
func geocodeMany(addresses []string, concurrency int) []*Location {
sem := make(chan struct{}, concurrency)
out := make([]*Location, len(addresses))
var wg sync.WaitGroup
for i, addr := range addresses {
wg.Add(1)
sem <- struct{}{} // acquire
go func(i int, addr string) {
defer wg.Done()
defer func() { <-sem }() // release
loc, err := geocode(addr, "US")
if err != nil {
fmt.Println("err:", addr, err)
return
}
out[i] = loc
}(i, addr)
}
wg.Wait()
return out
}Три идиома в този код оправдават мястото си. Първо, sem <- struct{}{} се случва *преди* ключовата дума go, така че for loop-ът блокира на throttle-а вместо да scheduler-ва хиляди pending goroutines. Второ, defer func() { <-sem }() освобождава slot-а дори ако worker-ът panic-не — верига от defer, която никога не бива да пропускаш. Трето, out[i] = loc пише на *уникален* index на goroutine, така че няма нужда от mutex; concurrent записи в различни slot-ове на slice са безопасни в Go. Capture-ването на i и addr като параметри на goroutine literal-а избягва loop-variable бъга, който е захапвал всеки Go разработчик преди 1.22.
Полезно стартово правило за concurrency: вземи per-minute rate limit на твоя план, раздели на 60 и цели приблизително толкова in-flight заявки. Free е 100/мин — започни с 4. Starter ($49, 1K/мин) — 16. Growth ($149, 5K/мин) — 64. Pro ($499, 10K/мин) — 128 и гледай headers-ите. Пълен разбор в concurrency tuning за geocoding.
Retry, backoff и context pattern-ът
Три неща, нужни на всеки production geocoder: (1) третирай 429 и 5xx като retriable, (2) уважавай Retry-After header-а, когато сървърът го праща, (3) ограничи опитите, за да не зацикля завинаги мъртъв ключ. Добави context.Context, за да може caller-ът да отмени дълга retry верига, и имаш функция, безопасна за пускане в реален service.
// retry.go
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
type GeocodeError struct {
Status int
Message string
}
func (e *GeocodeError) Error() string {
return fmt.Sprintf("geocode: status=%d msg=%s", e.Status, e.Message)
}
func geocodeWithRetry(ctx context.Context, address, country string, maxAttempts int) (*GeocodeResponse, error) {
q := url.Values{}
q.Set("q", address)
if country != "" {
q.Set("country", country)
}
target := baseURL + "/geocode?" + q.Encode()
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
req, err := http.NewRequestWithContext(ctx, "GET", target, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("CSV2GEO_KEY"))
resp, err := client.Do(req)
if err != nil {
// Network errors are retriable.
lastErr = err
if attempt == maxAttempts {
return nil, fmt.Errorf("network error after %d attempts: %w", attempt, err)
}
if err := sleep(ctx, backoff(attempt)); err != nil {
return nil, err
}
continue
}
if resp.StatusCode < 300 {
defer resp.Body.Close()
var out GeocodeResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
}
return &out, nil
}
// Non-retriable: 4xx other than 429 means bad key, bad input, etc.
if resp.StatusCode != 429 && resp.StatusCode < 500 {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return nil, &GeocodeError{Status: resp.StatusCode, Message: string(body)}
}
// Retriable: 429 or 5xx. Honour Retry-After if present.
retryAfter := resp.Header.Get("Retry-After")
resp.Body.Close()
if attempt == maxAttempts {
return nil, &GeocodeError{Status: resp.StatusCode, Message: "exhausted retries"}
}
delay := backoff(attempt)
if retryAfter != "" {
if secs, err := strconv.ParseFloat(retryAfter, 64); err == nil {
delay = time.Duration(secs * float64(time.Second))
}
}
if err := sleep(ctx, delay); err != nil {
return nil, err
}
}
return nil, fmt.Errorf("unreachable: lastErr=%v", lastErr)
}
func backoff(attempt int) time.Duration {
// 1, 2, 4, 8, 16... seconds + up to 1s jitter.
base := time.Duration(1<<(attempt-1)) * time.Second
jitter := time.Duration(rand.Float64() * float64(time.Second))
return base + jitter
}
func sleep(ctx context.Context, d time.Duration) error {
t := time.NewTimer(d)
defer t.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-t.C:
return nil
}
}Няколко бележки по дизайна. time.NewTimer плюс select на ctx.Done() е каноничният начин да имплементираш cancellable sleep — time.Sleep игнорира context и ще задържи твоята goroutine забодена през deploy или SIGTERM. Jitter-ът в backoff има значение, когато много workers ретраят едновременно; без него всеки worker се събужда в същия момент и прави re-DDoS на сървъра. Виж exponential backoff: when to retry, when to stop за математиката зад retry budgets и dead-letter queues.
Три rate-limit header-а се връщат на всеки успешен отговор и си струва да бъдат проверявани в production:
| Header | Значение | |---|---| | X-RateLimit-Limit | Per-minute таванът на твоя план | | X-RateLimit-Remaining | Какво ти остава в този window | | X-RateLimit-Reset | Unix секунди до reset на window-а | | Retry-After | Праща се на 429 — секунди да чакаш преди retry |
Ако Remaining падне под 10% от Limit, забави се доброволно — sleep, свали семафора, превключи на batch. По-евтино от трошене на 429-ки. По-задълбочена теория за това кой limiter algorithm произвежда тези headers има в token bucket vs leaky bucket vs sliding window.
Batch endpoint-ът
Subscription tier-ите от Starter ($49/мес) нагоре излагат batch endpoint, който приема масив от адреси в един POST. Лимити на плана:
| План | Месечни редове | Per-minute | Размер на batch | |---|---|---|---| | Free | — (1K/ден API, 100/ден batch) | 100 | 100 | | Starter ($49) | 50 000 | 1 000 | 1 000 | | Growth ($149) | 250 000 | 5 000 | 5 000 | | Pro ($499) | 1 000 000 | 10 000 | 10 000 |
// batch.go
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
type BatchRequest struct {
Addresses []string `json:"addresses"`
}
type BatchResponse struct {
Results []GeocodeResponse `json:"results"`
}
var batchClient = &http.Client{Timeout: 60 * time.Second}
func batchGeocode(ctx context.Context, addresses []string) (*BatchResponse, error) {
body, err := json.Marshal(BatchRequest{Addresses: addresses})
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", baseURL+"/geocode", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("CSV2GEO_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := batchClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return nil, fmt.Errorf("batch http %d", resp.StatusCode)
}
var out BatchResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
}
return &out, nil
}Batch отговорите запазват реда на входа: позиция N в отговора винаги отговаря на позиция N от входа. Не е нужен round-trip с поле id. Забележи специалния batchClient с 60-секунден timeout — batch от 1000 адреса на Growth отнема 10–15 секунди end-to-end, доста по-дълго от 10-секундния per-request клиент за singles.
Streaming на CSV без OOM
Ако входният файл е 100 000 реда, не искаш да го прочетеш в [][]string и да правиш range по него. Стриймвай го. Pattern-ът е worker pool: една goroutine чете редове от диск и ги тика в jobs channel, N worker goroutines консумират channel-а и пишат резултати в output channel, и една writer goroutine drain-ва output channel-а към диска.
// stream.go
package main
import (
"context"
"encoding/csv"
"fmt"
"io"
"os"
"sync"
)
type job struct {
ID string
Address string
}
type result struct {
ID string
Address string
Lat float64
Lng float64
Err string
}
const concurrency = 8
func streamGeocode(ctx context.Context, inPath, outPath string) error {
in, err := os.Open(inPath)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(outPath)
if err != nil {
return err
}
defer out.Close()
reader := csv.NewReader(in)
writer := csv.NewWriter(out)
defer writer.Flush()
header, err := reader.Read()
if err != nil {
return err
}
idIdx, addrIdx := indexOf(header, "id"), indexOf(header, "address")
if idIdx < 0 || addrIdx < 0 {
return fmt.Errorf("csv must have id,address columns")
}
if err := writer.Write([]string{"id", "address", "lat", "lng", "error"}); err != nil {
return err
}
jobs := make(chan job, concurrency*2)
results := make(chan result, concurrency*2)
var wg sync.WaitGroup
for i := 0; i < concurrency; i++ {
wg.Add(1)
go worker(ctx, &wg, jobs, results)
}
// Writer goroutine drains results to disk.
done := make(chan struct{})
go func() {
for r := range results {
_ = writer.Write([]string{
r.ID, r.Address,
fmt.Sprintf("%f", r.Lat), fmt.Sprintf("%f", r.Lng), r.Err,
})
}
close(done)
}()
// Producer: read CSV row by row.
for {
row, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
close(jobs)
wg.Wait()
close(results)
<-done
return err
}
jobs <- job{ID: row[idIdx], Address: row[addrIdx]}
}
close(jobs)
wg.Wait()
close(results)
<-done
return nil
}
func worker(ctx context.Context, wg *sync.WaitGroup, jobs <-chan job, results chan<- result) {
defer wg.Done()
for j := range jobs {
out, err := geocodeWithRetry(ctx, j.Address, "US", 5)
if err != nil {
results <- result{ID: j.ID, Address: j.Address, Err: err.Error()}
continue
}
if len(out.Results) == 0 {
results <- result{ID: j.ID, Address: j.Address, Err: "no_match"}
continue
}
top := out.Results[0]
if top.AccuracyScore < 0.7 {
results <- result{ID: j.ID, Address: j.Address, Err: "low_confidence"}
continue
}
results <- result{
ID: j.ID, Address: j.Address,
Lat: top.Location.Lat, Lng: top.Location.Lng,
}
}
}
func indexOf(row []string, name string) int {
for i, c := range row {
if c == name {
return i
}
}
return -1
}
func main() {
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "usage: stream input.csv output.csv")
os.Exit(1)
}
if err := streamGeocode(context.Background(), os.Args[1], os.Args[2]); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}Три неща, които този скрипт прави, а кодът на начинаещите обикновено не. Първо, входът се чете ред по ред, никога не се зарежда наведнъж. Второ, channel буферите са малки (concurrency*2) така producer-ът естествено backpressure-ва workers — ако всички workers са заети, producer-ът блокира на channel send и ОС държи файла най-вече на диска. Трето, грешките се записват като колона, а не свалят целия job. Това е разликата между скрипт, който завършва 100K-row run, и скрипт, който трябва да се рестартира с въпрос къде-спря.
Типизирани отговори
Struct-овете в началото на този пост са минимално жизнеспособната типизация. Ако искаш пълно покритие с optional полета, omitempty е твой приятел — държи JSON output чист, когато поле липсва, и избягва zero-value объркването при decoding на отговор.
type Components struct {
HouseNumber string `json:"house_number,omitempty"`
Street string `json:"street,omitempty"`
City string `json:"city,omitempty"`
State string `json:"state,omitempty"`
PostalCode string `json:"postal_code,omitempty"`
Country string `json:"country,omitempty"`
}
type Meta struct {
ResponseTimeMs int `json:"response_time_ms"`
Source string `json:"source"`
}
type FullGeocodeResult struct {
FormattedAddress string `json:"formatted_address"`
Location Location `json:"location"`
Accuracy string `json:"accuracy"`
AccuracyScore float64 `json:"accuracy_score"`
Components Components `json:"components"`
}
type FullGeocodeResponse struct {
Query string `json:"query"`
Results []FullGeocodeResult `json:"results"`
Meta Meta `json:"meta"`
}Дефинирането на Accuracy като typed alias (type Accuracy string плюс const declarations за всяка стойност) ти дава безплатен domain check за switch statements. Компилаторът няма да хване typo в string литерал, но ако switch-неш на Accuracy, необработена стойност поне се вижда в diff-а.
Неща, които захапват Go разработчиците
Забравяне на defer resp.Body.Close(). Всеки http.Client.Do, който връща non-nil response, *задължително* трябва да бъде последван от Close на body-то, дори ако никога не четеш body-то. Go runtime-ът няма да върне лежащото отдолу TCP съединение в connection pool-а, докато body-то не бъде затворено и drained. Пропусни го веднъж и под натиск твоят сервиз ще изтича file descriptor-и, докато kernel-ът не откаже нови sockets.
Използване на default http.Client. http.Get и компания използват http.DefaultClient, който има Timeout: 0. Лошо държащ се сървър може да остави тази goroutine блокирана неопределено дълго. Винаги декларирай package-level клиент с явен timeout или използвай http.NewRequestWithContext с deadline. Същото важи и за историята за p99 latency — ако нямаш timeout, нямаш p99, имаш опашка, която тича до безкрайност.
Deadlock-и на unbuffered channel. Ако worker чете от results, а writer goroutine не е стартирала, изпращането блокира. Ако producer чете от jobs, а няма жив worker, изпращането блокира. Буферът concurrency*2 в streaming примера е достатъчен за forward progress; избери буфер, който държи producer-а ~1 стъпка пред workers без да съхранява целия вход.
Наивни WaitGroup + channel паттерни. Често срещан бъг: затваряне на results channel-а преди workers да завършат, така че writer goroutine вижда затворен channel и излиза с partial output. Корекцията е явният ред в края на streamGeocode: close(jobs) първо (workers излизат, виждайки затворения jobs channel), после wg.Wait(), за да се потвърди, че всички workers са готови, после close(results), после <-done, за да изчака writer-а. Сбъркаш ли този ред — ще видиш missing-rows бъгове, които се възпроизвеждат само на дълги входове.
Често задавани въпроси
Нужен ли е SDK?
Не. Стандартната библиотека — net/http, encoding/json, context, time — покрива всичко в този пост. SDK-то, което повечето екипи в крайна сметка пишат, е тънък wrapper около geocodeWithRetry, набор от struct-ове в стил Pydantic и worker pool helper. Поддръжката на ~150 реда твой код излиза по-евтино от проследяване на ритъма на release на трета страна и vendored breaking change на всеки шест месеца.
Да третирам ли грешките като стойности?
Да — това е идиоматично Go. Типът *GeocodeError в retry функцията носи HTTP status като типизирано поле, така че caller-ите могат да правят switch по него без парсване на string-ове. Опаковай мрежови грешки с fmt.Errorf("...: %w", err), за да продължат errors.Is и errors.As да работят през call stack-а. Избягвай panic за всичко, което пресича API boundary; запази panic-ите за истински programmer бъгове.
net/http срещу fasthttp?
Остани с net/http за geocoding клиенти. fasthttp е по-бърз на ниво microbenchmark, но има различен API, не поддържа HTTP/2 и преизползва request/response обекти между goroutines по начини, които захапват, когато ги подадеш на worker pool. Bottleneck-ът на geocoding pipeline е network round trip-а, не HTTP parser-ът — fasthttp ще ти спести наносекунди, докато API отнема стотици милисекунди.
Как да тюнвам concurrency?
Вземи per-minute rate limit на твоя план, раздели на 60 и цели приблизително толкова concurrent заявки. 1000/мин ÷ 60 ≈ 17, така че pool size 16 е безопасен default на Starter. Източникът на истина е header-ът X-RateLimit-Remaining — логвай го на всеки успешен отговор и гледай дали не пада по-бързо от 1 на заявка. Оптималната concurrency е крива, и коляното обикновено е близо до (rate_limit / 60) * 1.5. Пълна методология в concurrency tuning за geocoding.
Как да отлича no-match от успешен low-confidence match?
results е празен slice при no-match. На low-confidence match results[0] съществува, но accuracy е "postcode" или "place" вместо "houseNumber" или "street", и accuracy_score е доста под 1.0. Разумен default threshold е 0.7; под него — третирай като no-match. По-стриктни pipelines (insurance risk scoring, healthcare patient mapping) използват 0.95 и изискват accuracy == "houseNumber". Пълната картина е в geocoding confidence scores explained.
Работи ли това за non-US адреси?
Да. Подай правилния ISO alpha-2 в параметъра country — DE за Германия, GB за UK, BR за Бразилия, JP за Япония. Покритието обхваща 39 страни днес, включително пълния top 10 по брой адреси: USA (121M адреса), Бразилия (90M), Мексико (30M), Франция (26M), Италия (26M), и останалите. Страницата на API изброява per-country counts.
Single заявки или batch endpoint да използвам?
Batch, когато имаш ≥100 адреса наведнъж и планът ти позволява (Starter+). Един POST е по-евтин и за двете страни от 100 GET-а, а икономията на latency е приблизително avg_per_request_ms * count / concurrency. Singles са по-прости за streaming workloads, при които адресите пристигат с времето, и singles са единствената опция на free tier. Пълният tradeoff анализ е в batch vs realtime geocoding.
Накъде оттук
Пълният reference на API е на csv2geo.com/api. Ако предпочиташ да работиш на друг език, Node.js geocoding tutorial следва същата структура с fetch и p-limit, а Python geocoding tutorial покрива httpx и asyncio. За по-задълбочени гмуркания в паттерните, които този пост докосва: rate limiting algorithms compared, concurrency tuning за geocoding, и защо p99 latency има значение повече от средното.
Ако намериш edge case, удариш форма на отговор, която този пост не покрива, или искаш feedback на Go pipeline, който строиш — формата за контакт на сайта стига до жив човек, който я чете. Bug report-и с curl (или go run) възпроизводими казуси се оправят бързо.
I.A. / CSV2GEO Creator
Свързани статии
- Node.js Geocoding API Tutorial: Concurrency, Retries и CSV Streaming
- Python Geocoding API Tutorial: Async, Retries и Pipeline на 100K реда
- Concurrency Tuning за Geocoding: намери своя Sweet Spot
- Rate Limiting на Geocoding Pipeline: Token Bucket vs Leaky Bucket vs Sliding Window
- p99 Latency в Geocoding: защо средното лъже
- Monorepo паттерни за multi-language Geocoding клиенти
Use our batch geocoding tool to convert thousands of addresses to coordinates in minutes. Start with 100 free addresses.
Try Batch Geocoding Free →