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
{
"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
attributionnames it. When nothing is found,attributionnames 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_limitedwithRetry-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_unavailablewithRetry-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_unavailableand 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.
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)'
fiThe 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.
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"]))'
fiWhen 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.