Skip to content
CRATON Docs Get a free API key

Exposure data

Geocoding: from an address to a point you choose

GET /geocode, with q in the query, turns an address or a place name into a short list of candidate points. You read the list and pick one. Every candidate says how precise it is and which public geocoder produced it, so a point in the middle of a street is never passed off as a building.

Nothing is written anywhere by a lookup. Ingest still never geocodes a file on its own, and a location without coordinates stays that way: the point you choose goes where you put it. Read the quickstart first for the two values you export (CEDE_BASE_URL, CEDE_API_KEY) and how to get a key.

On this page

What it is for, and what it is not

A schedule often names its buildings by address, and a point hazard lookup needs a coordinate. This route closes that gap one address at a time, with a person in the loop.

  • Not a silent step. The answer is candidates, not a decision. The route never picks one for you and never writes one anywhere.
  • Not a batch service. One address or place per request, for a person who submits it. Geocoding a whole schedule through this route is not what it is for, and the limits below are set for a person, not a loop.
  • Not autocomplete. Send the finished text once. Calling it on every keystroke would break the usage policy of the service behind it.

The two parameters

Parameter What it is
q The address or place, 1 to 200 characters. Runs of spaces and line breaks are collapsed to one space before anything else happens.
limit How many candidates at most, 1 to 10. Leave it out for 5.

Nothing else is read. Any other parameter, a repeated one, an empty q or one over 200 characters is refused with 400 invalid_request, and the message says what would work.

What comes back

JSON
{
  "schema_version": "0.1.0",
  "query": "1600 Pennsylvania Ave NW, Washington, DC 20500",
  "results": [
    {
      "label": "1600 PENNSYLVANIA AVE NW, WASHINGTON, DC, 20500",
      "lat": 38.8986989,
      "lon": -77.0351875,
      "precision": "address",
      "country_code": "US",
      "source": "us-census"
    }
  ],
  "attribution": [
    "Address matching by the U.S. Census Bureau Geocoder. Public domain."
  ]
}

That is a real answer, recorded from the U.S. Census Bureau Geocoder. The published schema is the whole contract.

Field What it is
query The text that was looked up, after whitespace was collapsed.
results Candidates in the order the geocoder ranked them, at most limit. Empty when every geocoder asked found nothing; that is a 200, not an error.
results[].label The geocoder's own name for what it matched. Read it before choosing: it is how you tell two candidates of the same name apart.
results[].lat, results[].lon Decimal degrees, WGS 84, at most seven decimal places.
results[].precision What the point stands for. See the next table.
results[].country_code ISO 3166-1 alpha-2, upper case, or null when the geocoder names no country.
results[].source us-census or nominatim: which geocoder produced this candidate.
attribution One line for each geocoder whose answer this is. Show it wherever you show the results.

Precision, finest first

precision The point is
address one building or address point. From the U.S. Census Bureau Geocoder it is placed along the street segment's range of house numbers, not on a rooftop.
street somewhere on a street, or a small named place
postcode the middle of a postal code area
locality a town, city, neighborhood or named square
region a county, state or province
country a whole country, or anything larger

Precision is never finer than the geocoder's own ranking supports, and it is made coarser when the matched feature is bigger than the level would suggest: a "building" half a kilometer across is reported as street. A postal code can match in more than one country (33139 is Miami Beach and a town in Lithuania), and both come back, so check country_code and label.

Which geocoder answers

  • An address that looks like a US street address (a house number first, then a US state, a ZIP code or the country after a comma) goes to the U.S. Census Bureau Geocoder.
  • Everything else goes to OpenStreetMap Nominatim, and so does a US address the Census geocoder cannot match.
  • Candidates always come from one geocoder, never a mix, and attribution names it. When nothing is found, attribution names every geocoder that was asked.
  • If one of them could not be reached and the other found nothing, the answer is 503 geocoder_unavailable, not an empty list: "nothing found" would be a claim the unreachable one never made.

Limits, and when it is off

  • Per key: 20 lookups a minute by default, on top of the API's own per-key limit. Over it, the answer is 429 rate_limited with Retry-After. A malformed request does not count.
  • Across all of Craton: Nominatim is asked at most once a second, whoever is asking. A lookup waits a moment for its turn; if the queue is longer than that, the answer is 503 geocoder_unavailable with Retry-After.
  • Repeats: an answer is kept for a day, so the same text asked again is answered without asking anyone.
  • Switched off: geocoding is off unless the environment switches it on. Off, every well-formed lookup answers 503 geocoder_unavailable and nothing leaves Craton.

Where your query goes

Looking something up sends the text of q to a service outside Craton:

  • a US street address goes to the U.S. Census Bureau Geocoder (geocoding.geo.census.gov), run by the United States government;
  • everything else, and any US address the Census geocoder cannot match, goes to OpenStreetMap Nominatim (nominatim.openstreetmap.org), run by the OpenStreetMap Foundation under its own usage and privacy policies.

Those services see the text and the Craton server that asked. They never see your key, your account or your own address. Craton keeps the answer in memory for up to a day and keeps the text out of its own access log; a lookup writes nothing to your account or to the database. Send the location of the thing you are analyzing, never the name of a person.

Where the data comes from

The U.S. Census Bureau Geocoder matches against the Census Bureau's address ranges and is in the public domain. Nominatim searches OpenStreetMap data, © OpenStreetMap contributors, available under the Open Database License (ODbL); the attribution line it requires comes back with every answer it gives.


Walk it

The first block looks up a US street address, which the Census geocoder answers. If the environment you point it at has geocoding switched off, or its geocoders did not answer, it prints the refusal instead, which is the other answer this route is documented to give.

Shell Runs in CI
set -euo pipefail

: "${CEDE_BASE_URL:?export CEDE_BASE_URL first, see the quickstart}"
: "${CEDE_API_KEY:?export CEDE_API_KEY first, see the quickstart}"

# 1: a US street address, answered by the U.S. Census Bureau Geocoder.
status=$(curl -sS -o white-house.json -w '%{http_code}' -G \
  "$CEDE_BASE_URL/geocode" \
  --data-urlencode 'q=1600 Pennsylvania Ave NW, Washington, DC 20500' \
  -H "Authorization: Bearer $CEDE_API_KEY")

if test "$status" = 503; then
  # Geocoding is off here, or no geocoder answered: the documented refusal.
  python3 -c 'import json
error = json.load(open("white-house.json"))["error"]
print(error["status"], error["code"])
print(error["message"])'
else
  python3 -c 'import json
answer = json.load(open("white-house.json"))
print("looked up  :", answer["query"])
for place in answer["results"]:
    print("candidate  :", place["label"])
    print("  point    :", place["lat"], place["lon"])
    print("  precision:", place["precision"], "from", place["source"])
for line in answer["attribution"]:
    print("attribution:", line)'
fi

The second block looks up a city outside the US, which Nominatim answers, and asks for at most three candidates. Read the precision beside each one: the city and the district around it are both called Kathmandu, and only the precision tells them apart.

Shell Runs in CI
set -euo pipefail

: "${CEDE_BASE_URL:?export CEDE_BASE_URL first, see the quickstart}"
: "${CEDE_API_KEY:?export CEDE_API_KEY first, see the quickstart}"

# 2: a place outside the US, answered by OpenStreetMap Nominatim.
status=$(curl -sS -o kathmandu.json -w '%{http_code}' -G \
  "$CEDE_BASE_URL/geocode" \
  --data-urlencode 'q=Kathmandu' --data-urlencode 'limit=3' \
  -H "Authorization: Bearer $CEDE_API_KEY")

if test "$status" = 503; then
  python3 -c 'import json
error = json.load(open("kathmandu.json"))["error"]
print(error["status"], error["code"])
print(error["message"])'
else
  python3 -c 'import json
answer = json.load(open("kathmandu.json"))
assert len(answer["results"]) <= 3
for place in answer["results"]:
    print(place["precision"].ljust(9), place["country_code"], place["label"])
print("attribution:", " ".join(answer["attribution"]))'
fi

When you have picked a candidate, its two numbers are the lat and lon of a point hazard lookup, or the coordinates of a location in an object you send to POST /objects. PATCH /objects/{id} does not write exposure, so a location an ingested object already holds keeps exactly what its file said.

This page as Markdown, byte for byte: /geocode.md