Go Geocoding チュートリアル:Goroutines、Bounded Concurrency、Backoff
本番向けの Go geocoding チュートリアル:goroutine pool、semaphore-bounded concurrency、429 への exponential backoff。コンパイルして動きます。
Go は、async/await の儀式なしに concurrency が欲しいときに手を伸ばすべき言語です。Goroutines は 2 KB のスタック、channels は型付きパイプ、そして標準ライブラリの net/http は最初の日からプロダクションで十分使えます。学ぶべき event loop はなく、すべての関数に振りまく await キーワードもなく、coloured-function 問題もありません。直線的なコードを書き、go と semaphore channel で展開します。
これは REST API 経由でアドレスを geocoding する実動 Go チュートリアルです。すべての snippet は .go ファイルに書き、go build を通し、公開前にクリーンな Go 1.22 インストール上で実 API に対して実行しました。SDK もサードパーティ HTTP ライブラリも framework もなく — net/http、encoding/json、encoding/csv、context だけです。終わりまでに、任意のサイズの CSV をストリーミングし、設定可能な goroutine pool で行を geocoding し、Retry-After を尊重する jittered exponential backoff で 429 と 5xx を retry し、入力ファイルをメモリに保持せずに {lat, lng, error} CSV を書くプログラムができます。約 80 行の Go です。
全体で使う endpoint は csv2geo.com/api/v1 です。free tier は 1 日 1,000 forward/reverse リクエストに加えて 1 日 100 batch 行で、クレジットカード不要です。サインインして /api-keys からキーを取得して進めてください。
Endpoint
ほぼすべての実世界の geocoding ジョブを 2 つの endpoint がカバーします。Forward はアドレス文字列を座標に変換します。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 リクエストのレスポンス形は次のようになります。これはドキュメント例ではなく実際の出力です。
{
"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" }
}もっとも重要なのは 2 つのフィールドです。results[0].location はあなたの {lat, lng} です。results[0].accuracy はマッチレベルで — "houseNumber" は rooftop、"street" は通り中心、"place" は POI マッチ、"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 接続がプールに戻されず、負荷下で file descriptor をリークします。直接 http.Client を使うのではなく単一の package-level の http.Get を使ってください; default クライアントは Timeout: 0 で、ハングしたサーバーが goroutine を永遠にブロックする可能性があります。キーが shell history や log ファイルに残らないように、Authorization: Bearer を query 形式の ?api_key= の代わりに使ってください。
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 フィールドが含まれます — 一致したアドレスが入力座標からどれだけ離れているかです。delivery アプリの GPS pings を reverse-geocoding する場合、~50m を超えるものは通常 GPS の fix が悪かったことを意味し、geocoder のせいではありません。~10m 未満なら rooftop を見ています。Reverse geocoding における唯一の正直な精度メトリックは ground-truth distance であり、それ以外はすべて少し嘘をつきます。
Go 流の bounded concurrency
Go で 10,000 アドレスを geocoding する間違った方法は for _, a := range addrs { go geocode(a) } です。これは 10,000 の unbounded goroutines を発射し、10,000 の TCP 接続を開き、2 回目の反復で API rate limit を使い切り、同時にローカルの file descriptor も使い切ります。Goroutines は安いですが、無料ではなく、ネットワークが無料になることもありません。
正しい方法は semaphore channel による bounded concurrency です。容量 struct{} の N 上限の buffered channel は Go の正典の semaphore です — そこに送信すると、すでに N 個の goroutines が in-flight なら blocking します。
// 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
}このコードの 3 つのイディオムが場所を勝ち取っています。第一に、sem <- struct{}{} は go キーワードの *前* に発生するので、for ループは保留中の数千の goroutines をスケジュールするのではなく、throttle でブロックします。第二に、defer func() { <-sem }() は worker が panic した場合でも slot を解放します — 決してスキップしてはならない defer のチェーンです。第三に、out[i] = loc は goroutine ごとに *一意* の index に書き込むので、mutex は不要です; Go では slice の異なる slot への concurrent write は安全です。goroutine リテラルのパラメータとして i と addr をキャプチャすることで、1.22 以前のすべての Go 開発者を噛んだ loop-variable バグを回避できます。
concurrency の有用な開始ルール: concurrency あなたのプランの per-minute レート制限を取り、60 で割り、おおよそその数の in-flight リクエストを目指します。Free は 100/min — 4 から始めてください。Starter ($49, 1K/min) — 16。Growth ($149, 5K/min) — 64。Pro ($499, 10K/min) — 128 で、ヘッダーを監視します。詳細な内訳は concurrency tuning for geocoding にあります。
Retry、backoff、context パターン
すべての production geocoder が必要とする 3 つのこと: (1) 429 と 5xx を retriable として扱う、(2) サーバーが送ってきたら Retry-After ヘッダーを尊重する、(3) 永久に死んだキーが永遠にループしないように試行回数を制限する。長時間の retry チェーンを caller がキャンセルできるよう context.Context を追加すれば、実サービスに投入できる安全な関数になります。
// 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() による select は cancellable sleep を実装する正典の方法です — time.Sleep は context を無視し、deploy や SIGTERM を通じて goroutine を固定したままにします。backoff の jitter は、多くの workers が同時に retry するときに重要です; なければ各 worker が同じ瞬間に目を覚まし、サーバーを re-DDoS します。retry budgets と dead-letter queues の数学については exponential backoff: when to retry, when to stop を参照してください。
成功したレスポンスごとに 3 つの rate-limit ヘッダーが返ってきて、production でチェックする価値があります:
| Header | 意味 | |---|---| | X-RateLimit-Limit | あなたのプランの per-minute 上限 | | X-RateLimit-Remaining | このウィンドウで残っているもの | | X-RateLimit-Reset | ウィンドウがリセットされるまでの Unix 秒 | | Retry-After | 429s で送信される — retry 前に待つ秒数 |
もし Remaining が Limit の 10% 未満に落ちたら、自発的にスローダウンしてください — sleep、semaphore を下げる、batch に切り替える。429 で叩きまくるより安いです。これらのヘッダーを生成する limiter algorithm のより深い理論は token bucket vs leaky bucket vs sliding window にあります。
Batch endpoint
Starter ($49/月) 以上のサブスクリプション tier では、1 つの POST で配列のアドレスを取る batch endpoint が利用できます。プラン上限:
| プラン | 月間行数 | 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 に対応します。id フィールドを往復させる必要はありません。専用の batchClient に 60 秒 timeout が設定されていることに注意してください — Growth での 1,000 アドレスの batch はエンドツーエンドで 10–15 秒かかり、singles に使う 10 秒の per-request クライアントよりはるかに長いです。
OOM なしの Streaming CSV
入力ファイルが 100,000 行なら、[][]string に読み込んで range で反復したくはありません。ストリーミングしてください。パターンは worker pool: 1 つの goroutine がディスクから行を読み jobs channel に push し、N 個の worker goroutines が channel を消費して結果を出力 channel に書き、1 つの writer goroutine が出力 channel をディスクに drain します。
// 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)
}
}このスクリプトが初心者のコードと違う 3 つの点。第一に、入力は行単位で読まれ、決して全体としてロードされません。第二に、channel バッファは小さく (concurrency*2)、producer は workers に自然に backpressure をかけます — すべての workers が忙しい場合、producer は channel send でブロックし、OS はファイルをほとんどディスク上に保ちます。第三に、エラーは job 全体をクラッシュさせるのではなく、列として記録されます。これが、100K 行の run を完走するスクリプトと、どこで止まったかと聞かれて再起動するスクリプトの違いです。
型付きレスポンス
この投稿の最初にある struct は最小限実行可能な型付けです。オプショナルフィールドで完全カバレッジが欲しいなら、omitempty が友達です — フィールドが存在しないとき JSON 出力をきれいに保ち、レスポンスのデコード時に zero-value の混乱を回避します。
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 を型付きエイリアス (type Accuracy string 各値の const 宣言を加えて) として定義すると、switch ステートメントの無料の domain check が得られます。コンパイラは文字列リテラルのタイポを捕まえませんが、Accuracy で switch すると、未処理の値は少なくとも diff で見えます。
Go 開発者を噛むこと
defer resp.Body.Close() を忘れること。 non-nil response を返すすべての http.Client.Do の後には *必ず* body の Close が続かなければなりません。body を一度も読まない場合でもです。Go ランタイムは body が close され drain されるまで底層の TCP 接続を connection pool に戻しません。一度スキップすると、kernel が新しい sockets を拒否するまで、負荷下のサービスは file descriptor をリークします。
デフォルトの http.Client を使うこと。 http.Get 系は http.DefaultClient を使い、それは Timeout: 0 を持ちます。お行儀の悪いサーバーは goroutine を無期限にブロック状態にする可能性があります。明示的な timeout を持つ package-level クライアントを常に宣言するか、deadline 付きで http.NewRequestWithContext を使ってください。同じことが p99 latency story にも当てはまります — timeout がなければ p99 もなく、無限に走るテールがあります。
バッファなし channel デッドロック。 worker が results から読み、writer goroutine が始まっていない場合、send はブロックします。producer が jobs から読み、worker が生きていない場合、send はブロックします。streaming 例の concurrency*2 バッファは forward progress に十分です; 入力全体を保存せず producer が workers より ~1 ステップ先を行ける buffer を選んでください。
ナイーブな WaitGroup + channel パターン。 一般的なバグ: workers が終わる前に results channel を閉じてしまい、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 は geocodeWithRetry の薄いラッパー、Pydantic 相当の struct セット、worker pool ヘルパーです。サードパーティのリリースケイデンスを追跡し、6 か月ごとに vendored breaking change を扱うより、自分の ~150 行のコードを保守する方が安いです。
エラーを値として扱うべきですか?
はい — それが慣用的な Go です。retry 関数の *GeocodeError 型は HTTP ステータスを型付きフィールドとして運ぶので、caller は文字列をパースせずに switch できます。ネットワークエラーは fmt.Errorf("...: %w", err) でラップして、errors.Is と errors.As が call stack を通して機能し続けるようにしてください。API 境界を越えるものに対しては panic を避けてください; panic は本物のプログラマのバグのために予約してください。
net/http と fasthttp?
geocoding クライアントには net/http のままでいてください。fasthttp はマイクロベンチマークレベルで速いですが、API が異なり、HTTP/2 をサポートせず、worker pool に渡したときに噛む方法で goroutines 間で request/response オブジェクトを再利用します。geocoding パイプラインのボトルネックはネットワーク往復であり、HTTP パーサーではありません — fasthttp は API が数百ミリ秒かかる間にナノ秒を節約してくれます。
concurrency をどうチューニングしますか?
あなたのプランの per-minute レート制限を取り、60 で割り、おおよそその数の concurrent リクエストを目指します。1,000/min ÷ 60 ≈ 17 なので、Starter での pool サイズ 16 は安全なデフォルトです。真実のソースは X-RateLimit-Remaining ヘッダーです — 成功した各レスポンスでログに記録し、リクエストあたり 1 より速く落ちないか監視してください。最適な concurrency は曲線で、エルボーは通常 (rate_limit / 60) * 1.5 付近です。完全な方法論は concurrency tuning for geocoding にあります。
no-match と成功した低 confidence マッチをどう区別しますか?
results は no-match では空のスライスです。低 confidence マッチでは results[0] は存在しますが accuracy は "postcode" または "place" で、"houseNumber" や "street" ではなく、accuracy_score は 1.0 を大きく下回ります。妥当なデフォルト threshold は 0.7 です; それ以下は no-match として扱ってください。より厳しいパイプライン (保険リスクスコアリング、ヘルスケア患者マッピング) は 0.95 を使い、accuracy == "houseNumber" を要求します。全体像は geocoding confidence scores explained にあります。
これは US 以外のアドレスでも機能しますか?
はい。country パラメータに正しい ISO alpha-2 を渡してください — ドイツは DE、英国は GB、ブラジルは BR、日本は JP です。カバレッジは今日 39 か国に及び、アドレス数のフルトップ 10 を含みます: USA (1.21 億アドレス)、ブラジル (9000 万)、メキシコ (3000 万)、フランス (2600 万)、イタリア (2600 万)、その他。API ページ に国別カウントが記載されています。
single リクエストと batch endpoint のどちらを使うべきですか?
100 アドレス以上を一度に処理し、プランが許可する場合 (Starter+) は batch を使ってください。1 つの POST は 100 GET よりも双方にとって安く、レイテンシ節約はおおよそ avg_per_request_ms * count / concurrency です。Singles はアドレスが時間とともに到着するストリーミングワークロードでよりシンプルで、free tier では singles のみが選択肢です。完全なトレードオフ分析は batch vs realtime geocoding にあります。
ここからどこへ
API の完全なリファレンスは csv2geo.com/api にあります。別の言語で作業したいなら、Node.js geocoding tutorial は fetch と p-limit で同じ構造に従い、Python geocoding tutorial は httpx と asyncio をカバーします。この投稿が触れるパターンへのより深い掘り下げは: rate limiting algorithms compared、concurrency tuning for geocoding、そして なぜ p99 latency が平均より重要なのか を参照してください。
エッジケースを見つけたり、この投稿がカバーしていないレスポンス形に当たったり、構築中の Go パイプラインへのフィードバックが欲しい場合、サイトの問い合わせフォームは読んでくれる人に届きます。curl (または go run) の再現を含むバグレポートはすぐに修正されます。
I.A. / CSV2GEO Creator
関連記事
- Node.js Geocoding API チュートリアル: Concurrency、Retries、CSV Streaming
- Python Geocoding API チュートリアル: Async、Retries、100K 行 CSV Pipeline
- Geocoding のための Concurrency Tuning: Sweet Spot を見つける
- Geocoding パイプラインの Rate Limiting: Token Bucket vs Leaky Bucket vs Sliding Window
- Geocoding における p99 Latency: 平均が嘘をつく理由
- マルチ言語 Geocoding クライアントのための Monorepo パターン
Use our batch geocoding tool to convert thousands of addresses to coordinates in minutes. Start with 100 free addresses.
Try Batch Geocoding Free →