Write icon
Get icon
If icon
Queries icon
Download icon
Set icon
Log icon

Batch Geocode US Addresses to Coordinates and Census Tracts with the Census Geocoder and a DuckDB Cache

Batch geocode US addresses with the free Census Geocoder, parse the CSV reply in DuckDB and cache results in Parquet so re-runs only send new addresses.

Categories
Data

Address lists are geocoded again and again: the customer file grows by a few hundred rows a week and the whole thing goes through a paid geocoder every time. The US Census Bureau runs a free geocoder with a batch endpoint that takes a CSV of up to 10,000 addresses per request and returns coordinates plus the state, county, tract and block codes that join directly to Census and ACS tables. This flow wraps it with a cache: DuckDB normalises the addresses into keys, removes the ones already answered, sends only the rest in a single multipart POST, parses the CSV reply and merges it into a Parquet cache kept in Kestra's internal storage. Every input row, duplicates included, comes back with its coordinates and a 15-digit block GEOID.

How it works

  1. The address list comes either from the addresses_csv input (inline CSV with a header row id,street,city,state,zip) or from an uploaded addresses_file. inline_addresses (Storage Write) puts the inline text in internal storage so both paths are read the same way.
  2. previous_cache (KV Get, errorOnMissing: false) reads the URI of the Parquet cache written by the last run; bootstrap (If) runs empty_cache (DuckDB Queries) on the first run only, writing an empty Parquet file with the cache schema so every later task has one code path.
  3. build_batch (DuckDB Queries) builds an address_key per row (upper case, trimmed, single spaces), keeps one row per key, drops the keys already in the cache and writes two files through outputFiles: the Census batch CSV (no header, batch id plus the four address fields, at most 10,000 rows) and a key map from batch id to address key. With recheck_no_match the cached No_Match and Tie rows are sent again. The result row counts input rows, unique addresses, cache hits, rows sent and rows deferred.
  4. geocode_new (If) runs only when there is something to send. geocode (HTTP Download) POSTs the batch file as multipart/form-data to geocoding.geo.census.gov/geocoder/geographies/addressbatch with the chosen benchmark and vintage; the reply, a CSV named GeocodeResults.csv, lands in internal storage. A long readIdleTimeout covers a full 10,000-row batch.
  5. merge_cache (DuckDB Queries) reads the reply with an explicit column list (No_Match and Tie rows carry only three fields), splits the longitude,latitude pair, joins the batch ids back to the address keys, replaces those keys in the cache and writes the new Parquet file. save_cache (KV Set) points the KV key at it.
  6. join_output (DuckDB Queries) left-joins every input row to the cache, in the original order, and writes the geocoded CSV: match status and type, standardised address, longitude, latitude, FIPS codes, block GEOID, benchmark, when the address was geocoded and whether it came from the cache. Rows beyond the 10,000 cap show as PENDING until the next run.
  7. summary logs the counts; the flow exposes the geocoded CSV, the two summary rows and the cache Parquet as outputs.

What you get

  • A geocoded CSV with coordinates and state, county, tract and block FIPS codes for every input row, ready to join to Census or ACS data.
  • A Parquet cache that makes the second run of the same list free and the run on a grown list cost only the new rows.
  • Duplicate and differently formatted spellings of the same address resolved once and answered for every row.
  • No_Match, Tie and PENDING statuses in the output instead of silently dropped rows.
  • No API key, no per-address loop, no database.

Who it's for

  • Analytics and data engineering teams enriching customer, store or facility addresses with Census geography for demographic joins.
  • Public health, housing and civic tech teams who map addresses to tracts and need reproducible vintages.
  • Anyone paying a commercial geocoder for US addresses that the Census Geocoder would resolve for free.

Why orchestrate this with Kestra

  • Download with formData uploads a file straight from internal storage as multipart/form-data; no script, no container.
  • DuckDB inputFiles and outputFiles carry the batch, the reply and the Parquet cache between tasks declaratively.
  • The KV store holds one URI per address list, so several lists or benchmarks share the flow under different cache_key values.
  • If conditions skip the bootstrap after the first run and the HTTP call when everything is cached.

Prerequisites

  • Kestra 2.0 with the JDBC DuckDB plugin.
  • Outbound HTTPS to geocoding.geo.census.gov; the Census Geocoder needs no key or account.
  • US addresses only (50 states, DC and Puerto Rico); the Census standardises them, so mixed case and extra spaces are fine.
  • The cache Parquet lives in the execution folder of the run that wrote it; if you purge executions, keep the latest successful one of this flow or copy the file first.

Secrets

  • None. The Census Geocoder is public and keyless.

Quick start

  1. Import the flow and click Execute with the defaults: six rows, five unique addresses (one is the White House written twice, once in lower case with stray spaces) and one street that does not exist. The first run bootstraps the cache, sends 5 addresses and returns 5 Match rows (the duplicate answered from the same lookup) and 1 No_Match.
  2. Execute again: 5 already cached, 0 sent, the HTTP call is skipped and the output CSV is rebuilt from the cache with from_cache = true.
  3. Add two addresses to addresses_csv and run: 5 cached, 2 sent.
  4. Run with recheck_no_match: true: only the No_Match address is sent again.
  5. Upload your own CSV as addresses_file (same header) for real lists; up to 10,000 new addresses go in one run, the rest show as PENDING and go in the next.

How to extend

  • Trigger on a new file in S3, GCS or SFTP with the matching trigger and pass it as addresses_file.
  • Load the geocoded CSV into Postgres, Snowflake or BigQuery with the corresponding plugin, or read the cache Parquet directly in DuckDB.
  • Join block_geoid or the tract code to ACS tables from api.census.gov for population and income enrichment.
  • Switch benchmark and vintage to Public_AR_Census2020 and Census2020_Census2020 under a separate cache_key to geocode against 2020 geography.
  • Add a Slack or email summary task after summary when no_match exceeds a threshold.

Links

Orchestrate with Kestra
Orchestrate DuckDB with Kestra
Share this Blueprint
See How

New to Kestra?

Use blueprints to kickstart your first workflows.