Geocoding Service
This service provides geocoding infrastructure for research purposes at SAFE. It is based on Pelias, an open-source geocoder using open geospatial data.
/v1/ require a personal API key.
Please include the key in the HTTP header X-API-Key. Do not publish API keys in GitHub,
public notebooks, shared scripts, replication packages, or papers.
/v1/search/structured with
size=1. Keep firm names separate from postal addresses, deduplicate
addresses before geocoding, and always store layer,
match_type, confidence, source,
gid, lon, and lat. Do not treat
country-, region-, or fallback-level results as precise point locations.
1. Quick start
The base URL of the SAFE geocoding service is:
https://geocode.safe-frankfurt.de
API requests must include your personal API key:
X-API-Key: YOUR_PERSONAL_API_KEY
Minimal example
curl -H "X-API-Key: YOUR_PERSONAL_API_KEY" \
"https://geocode.safe-frankfurt.de/v1/search?text=Frankfurt"
The service returns GeoJSON. The first result label can be inspected with:
curl -s -H "X-API-Key: YOUR_PERSONAL_API_KEY" \
"https://geocode.safe-frankfurt.de/v1/search?text=Frankfurt" \
| jq '.features[0].properties.label'
2. Which endpoint should I use?
If you are new to geocoding, start with this decision table.
| Your data situation | Recommended endpoint | Example |
|---|---|---|
| You have one address string per row. | /v1/search |
Neue Mainzer Strasse 52 Frankfurt Germany |
| You have separate columns for street, postal code, city, and country. | /v1/search/structured |
address, postalcode, locality, country |
| You already have coordinates and want to identify the nearest place or address. | /v1/reverse |
point.lat=50.1109&point.lon=8.6821 |
| You build an interactive application with typeahead search. | /v1/autocomplete |
Interactive user interface, not typical batch geocoding. |
/v1/search/structured. Structured input reduces ambiguity and makes the geocoding workflow easier to document.
3. Available endpoints
| Endpoint | Purpose | Typical use |
|---|---|---|
/v1/search |
Free-text forward geocoding | Use when you have one address string, e.g. from historical records or scraped data. |
/v1/search/structured |
Structured forward geocoding | Recommended when address parts are available separately, e.g. street, postal code, city, country. |
/v1/reverse |
Reverse geocoding | Use when you have coordinates and want the nearest place/address. |
/v1/autocomplete |
Autocomplete / typeahead | Useful for interactive applications, not usually needed for batch geocoding. |
/attribution |
Data attribution | Public endpoint listing attribution information for data providers. |
Free-text search
https://geocode.safe-frankfurt.de/v1/search?text=Frankfurt
Structured search
https://geocode.safe-frankfurt.de/v1/search/structured?address=Unter%20den%20Linden%2077&locality=Berlin&country=DE
Reverse geocoding
https://geocode.safe-frankfurt.de/v1/reverse?point.lat=50.1109&point.lon=8.6821
4. Examples: curl, Python, R
curl: free-text search
curl -s -H "X-API-Key: YOUR_PERSONAL_API_KEY" \
"https://geocode.safe-frankfurt.de/v1/search?text=Neue%20Mainzer%20Strasse%2052%20Frankfurt" \
| jq '.features[0]'
curl: structured search
curl -s -H "X-API-Key: YOUR_PERSONAL_API_KEY" \
"https://geocode.safe-frankfurt.de/v1/search/structured?address=Neue%20Mainzer%20Strasse%2052&locality=Frankfurt&country=DE&size=1" \
| jq '.features[0].properties'
curl: request only selected output fields
curl -s -H "X-API-Key: YOUR_PERSONAL_API_KEY" \
"https://geocode.safe-frankfurt.de/v1/search/structured?address=Neue%20Mainzer%20Strasse%2052&locality=Frankfurt&country=DE&size=1" \
| jq '.features[0] | {
label: .properties.label,
layer: .properties.layer,
source: .properties.source,
gid: .properties.gid,
match_type: .properties.match_type,
confidence: .properties.confidence,
coordinates: .geometry.coordinates
}'
Python: single request
import requests
API_KEY = "YOUR_PERSONAL_API_KEY"
BASE_URL = "https://geocode.safe-frankfurt.de"
headers = {
"X-API-Key": API_KEY
}
params = {
"address": "Neue Mainzer Strasse 52",
"locality": "Frankfurt",
"country": "DE",
"size": 1
}
response = requests.get(
f"{BASE_URL}/v1/search/structured",
headers=headers,
params=params,
timeout=30
)
response.raise_for_status()
data = response.json()
if not data.get("features"):
raise ValueError("No geocoding result returned")
feature = data["features"][0]
print(feature["properties"]["label"])
print(feature["properties"].get("layer"))
print(feature["properties"].get("match_type"))
print(feature["geometry"]["coordinates"]) # [lon, lat]
Python: batch geocoding from a CSV file
This example assumes a CSV file with the columns id, address,
postalcode, locality, and country.
import time
from datetime import date
import pandas as pd
import requests
API_KEY = "YOUR_PERSONAL_API_KEY"
BASE_URL = "https://geocode.safe-frankfurt.de"
headers = {"X-API-Key": API_KEY}
df = pd.read_csv("addresses.csv")
results = []
for _, row in df.iterrows():
params = {
"address": row.get("address"),
"postalcode": row.get("postalcode"),
"locality": row.get("locality"),
"country": row.get("country"),
"size": 1,
}
# Remove empty parameters
params = {
k: v for k, v in params.items()
if pd.notna(v) and str(v).strip()
}
try:
r = requests.get(
f"{BASE_URL}/v1/search/structured",
headers=headers,
params=params,
timeout=30
)
r.raise_for_status()
data = r.json()
if data.get("features"):
f = data["features"][0]
props = f["properties"]
lon, lat = f["geometry"]["coordinates"]
layer = props.get("layer")
match_type = props.get("match_type")
is_fallback = (match_type == "fallback") or (layer in ["region", "country"])
is_high_quality = (layer == "address" and match_type in ["exact", "interpolated"])
results.append({
"id": row["id"],
"found": True,
"label": props.get("label"),
"layer": layer,
"source": props.get("source"),
"gid": props.get("gid"),
"match_type": match_type,
"confidence": props.get("confidence"),
"lon": lon,
"lat": lat,
"is_high_quality": is_high_quality,
"is_fallback": is_fallback,
"geocoding_endpoint": "/v1/search/structured",
"geocoding_date": date.today().isoformat(),
"error": None,
})
else:
results.append({
"id": row["id"],
"found": False,
"label": None,
"layer": None,
"source": None,
"gid": None,
"match_type": None,
"confidence": None,
"lon": None,
"lat": None,
"is_high_quality": False,
"is_fallback": False,
"geocoding_endpoint": "/v1/search/structured",
"geocoding_date": date.today().isoformat(),
"error": "No result",
})
except Exception as e:
results.append({
"id": row["id"],
"found": False,
"error": str(e),
"geocoding_endpoint": "/v1/search/structured",
"geocoding_date": date.today().isoformat(),
})
# Be polite to the service for larger jobs.
time.sleep(0.1)
out = pd.DataFrame(results)
out.to_csv("addresses_geocoded.csv", index=False)
R: single request
library(httr2)
api_key <- "YOUR_PERSONAL_API_KEY"
base_url <- "https://geocode.safe-frankfurt.de"
req <- request(paste0(base_url, "/v1/search/structured")) |>
req_headers("X-API-Key" = api_key) |>
req_url_query(
address = "Neue Mainzer Strasse 52",
locality = "Frankfurt",
country = "DE",
size = 1
)
resp <- req_perform(req)
data <- resp_body_json(resp)
# In R, list indices start at 1.
feature <- data$features[[1]]
feature$properties$label
feature$properties$layer
feature$properties$match_type
feature$properties$confidence
feature$geometry$coordinates # c(lon, lat)
R: batch geocoding from a CSV file
This example assumes a CSV file with the columns id, address,
postalcode, locality, and country.
It stores the first result per input row and writes a compact output table.
library(httr2)
library(readr)
library(dplyr)
library(purrr)
library(tibble)
library(lubridate)
api_key <- "YOUR_PERSONAL_API_KEY"
base_url <- "https://geocode.safe-frankfurt.de"
`%||%` <- function(x, y) if (is.null(x)) y else x
geocode_one <- function(id, address, postalcode = NA, locality = NA, country = NA) {
params <- list(
address = address,
postalcode = postalcode,
locality = locality,
country = country,
size = 1
)
# Remove empty values
params <- params[!map_lgl(params, ~ is.na(.x) || identical(.x, ""))]
tryCatch({
req <- request(paste0(base_url, "/v1/search/structured")) |>
req_headers("X-API-Key" = api_key) |>
req_url_query(!!!params)
resp <- req_perform(req)
data <- resp_body_json(resp)
if (length(data$features) == 0) {
return(tibble(
id = id,
found = FALSE,
label = NA_character_,
layer = NA_character_,
source = NA_character_,
gid = NA_character_,
match_type = NA_character_,
confidence = NA_real_,
lon = NA_real_,
lat = NA_real_,
is_high_quality = FALSE,
is_fallback = FALSE,
geocoding_endpoint = "/v1/search/structured",
geocoding_date = as.character(today()),
error = "No result"
))
}
feature <- data$features[[1]]
props <- feature$properties
coords <- feature$geometry$coordinates
layer <- props$layer %||% NA_character_
match_type <- props$match_type %||% NA_character_
is_fallback <- match_type == "fallback" || layer %in% c("region", "country")
is_high_quality <- layer == "address" && match_type %in% c("exact", "interpolated")
tibble(
id = id,
found = TRUE,
label = props$label %||% NA_character_,
layer = layer,
source = props$source %||% NA_character_,
gid = props$gid %||% NA_character_,
match_type = match_type,
confidence = props$confidence %||% NA_real_,
lon = coords[[1]],
lat = coords[[2]],
is_high_quality = is_high_quality,
is_fallback = is_fallback,
geocoding_endpoint = "/v1/search/structured",
geocoding_date = as.character(today()),
error = NA_character_
)
}, error = function(e) {
tibble(
id = id,
found = FALSE,
label = NA_character_,
layer = NA_character_,
source = NA_character_,
gid = NA_character_,
match_type = NA_character_,
confidence = NA_real_,
lon = NA_real_,
lat = NA_real_,
is_high_quality = FALSE,
is_fallback = FALSE,
geocoding_endpoint = "/v1/search/structured",
geocoding_date = as.character(today()),
error = conditionMessage(e)
)
})
}
addresses <- read_csv("addresses.csv", show_col_types = FALSE)
results <- pmap_dfr(
list(
id = addresses$id,
address = addresses$address,
postalcode = addresses$postalcode,
locality = addresses$locality,
country = addresses$country
),
function(id, address, postalcode, locality, country) {
Sys.sleep(0.1) # Be polite to the service for larger jobs.
geocode_one(id, address, postalcode, locality, country)
}
)
write_csv(results, "addresses_geocoded.csv")
5. Response object structure
Pelias returns GeoJSON. The top-level object contains metadata and a list of result
features. In most batch-geocoding workflows, researchers store the first result, but they should
also store diagnostic fields such as layer, match_type, confidence,
source, and gid.
[longitude, latitude], not [latitude, longitude].
Simplified response structure
{
"geocoding": {
"version": "...",
"attribution": "...",
"query": {
"text": "...",
"size": 10
}
},
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [8.6821, 50.1109]
},
"properties": {
"id": "...",
"gid": "...",
"layer": "address",
"source": "openstreetmap",
"source_id": "...",
"name": "...",
"label": "...",
"confidence": 1,
"match_type": "exact",
"accuracy": "point",
"country": "Germany",
"country_a": "DEU",
"region": "Hessen",
"locality": "Frankfurt",
"postalcode": "60311",
"street": "...",
"housenumber": "..."
}
}
],
"bbox": [...]
}
The exact set of fields can vary by result, data source, country, and result layer. An address result may contain street and house-number information, while a locality, region, or country result will not. Code should therefore handle missing fields.
Fields researchers should usually store
| Output column | JSON path | Why store it? |
|---|---|---|
label |
features[0].properties.label |
Human-readable result for manual checks. |
lon |
features[0].geometry.coordinates[0] |
Longitude. GeoJSON uses longitude first. |
lat |
features[0].geometry.coordinates[1] |
Latitude. GeoJSON uses latitude second. |
layer |
features[0].properties.layer |
Indicates whether the result is an address, street, venue, locality, region, or country. |
match_type |
features[0].properties.match_type |
Helps distinguish exact, interpolated, and fallback results. |
confidence |
features[0].properties.confidence |
Useful diagnostic score, but should not be used alone. |
source |
features[0].properties.source |
Underlying data source. Useful for diagnostics and reproducibility. |
gid |
features[0].properties.gid |
Pelias global identifier for the returned feature. |
Accessing the first result
| Language | First result | Coordinates |
|---|---|---|
| JSON / JavaScript / Python | features[0] |
features[0].geometry.coordinates |
| R | features[[1]] |
features[[1]]$geometry$coordinates |
6. Recommended workflow for research data
/v1/search/structured when your data has separate
fields for street/address, postal code, city/locality, region, or country.
Recommended input fields
| Input information | Pelias parameter | Example |
|---|---|---|
| Street and house number | address |
Neue Mainzer Strasse 52 |
| Postal code | postalcode |
60311 |
| City | locality |
Frankfurt |
| Region / state | region |
Hessen |
| Country | country |
DE |
Practical recommendation
- Prefer structured search if your data has separate address fields.
- Use country information where available, preferably as a two-letter ISO code such as
DE,US,GB,FR. - In output, Pelias may return three-letter country abbreviations such as
DEU. - Do not blindly trust the first result. Always inspect
layer,match_type,confidence, and the coordinates. - Treat country-level and region-level fallback results as low-confidence results.
- For large research projects, manually inspect a sample of matches before using the coordinates in analysis.
- For batch geocoding, consider setting
size=1if you only want to store the top result.
Recommended compact output table
For research projects, do not only save latitude and longitude. Save input fields, output fields, quality flags, and reproducibility metadata.
original_id
input_address
input_postalcode
input_locality
input_country
found
label
layer
match_type
confidence
source
gid
lon
lat
is_high_quality
is_fallback
geocoding_endpoint
geocoding_date
error
7. Research design checklist before geocoding
Before running a large geocoding job, clarify how precise the coordinates need to be for your research design. This is especially important when geocoding firm, bank, branch, plant, or facility locations and linking them to remote-sensing, climate, pollution, neighborhood, or regional datasets.
Questions to answer before geocoding
| Question | Why it matters | Example decision |
|---|---|---|
| Do I need building-, street-, city-, or region-level precision? | Determines whether address, street, locality, or region results are acceptable. |
For pixel-level pollution exposure, keep only high-quality address-level results. |
| Am I geocoding headquarters, plants, branches, stores, or legal registration addresses? | Different address concepts imply different economic interpretations. | Do not interpret headquarters coordinates as production-site exposure. |
| Are the addresses historical? | Current geospatial data may not represent historical street names, borders, or city structures. | Flag historical addresses and validate a larger sample manually. |
| Will I merge to fine-grained raster data? | Small coordinate errors can change the assigned pixel or exposure value. | Exclude fallbacks and inspect interpolated results. |
| What share of observations may remain ungeocoded? | Geocoding failure can create sample selection. | Report geocoding rates by country, year, sector, and data source. |
Suggested pre-analysis plan note
We classify geocoding results before merging to spatial data. Address-level exact
and interpolated matches are treated as high quality. Street-, venue-, and locality-level
matches are used only where the research design permits coarser spatial precision.
Region-, country-, and fallback-level matches are flagged as low confidence and excluded
from point-level analyses.
8. Input preparation
Many geocoding problems are caused by noisy input data rather than by the geocoder itself. Cleaning and standardizing address fields before geocoding usually improves both match rates and interpretability.
Recommended preparation steps
| Step | Recommendation | Reason |
|---|---|---|
| Separate address concepts | Keep firm name, street address, postal code, city, region, and country in separate columns. | Structured geocoding works best with clean components. |
| Remove firm names from street-address fields | Use address for street and house number, not company names. |
Company names may trigger venue matches or unrelated places. |
| Standardize countries | Use two-letter country codes such as DE, US, GB, FR. |
Country information reduces ambiguity. |
| Preserve original strings | Never overwrite the raw input address. | Needed for auditing, correction, and replication. |
| Flag PO boxes and vague addresses | Create an indicator for PO boxes, missing street names, missing house numbers, and headquarters-only addresses. | These often do not represent precise physical locations. |
| Handle historical names carefully | Keep historical city/street names and, where possible, create modern equivalents in separate columns. | Current geocoders usually operate on current open geospatial data. |
Useful input table structure
original_id
firm_name
address_raw
address_clean
postalcode
locality
region
country
address_type
address_year
source_dataset
notes
address_raw and
address_clean. The cleaned address is used for geocoding, but the raw address
is needed for transparency and debugging.
9. Validation and diagnostics
After geocoding, do not go directly to the final analysis. First inspect match quality. This is where many avoidable measurement-error problems can be caught.
Minimum diagnostic checks
- Tabulate results by
found,layer, andmatch_type. - Check geocoding success rates by country, year, data source, and relevant sample groups.
- Inspect all
region,country, andfallbackresults before using them. - Manually review a random sample of high-, medium-, and low-quality matches.
- Check whether returned countries match the input countries.
- For remote-sensing applications, map a sample of points and compare them to the expected locations.
- Store all filtering decisions and report how many observations are dropped or flagged.
Python diagnostic summary
import pandas as pd
df = pd.read_csv("addresses_geocoded.csv")
print(pd.crosstab(df["layer"], df["match_type"], dropna=False))
print(df.groupby("input_country")["found"].mean().sort_values())
low_conf = df[
(df["is_fallback"] == True) |
(df["layer"].isin(["region", "country"]))
]
print(low_conf[["id", "label", "layer", "match_type", "confidence"]].head(50))
R diagnostic summary
library(readr)
library(dplyr)
df <- read_csv("addresses_geocoded.csv", show_col_types = FALSE)
df |>
count(layer, match_type, sort = TRUE)
df |>
group_by(input_country) |>
summarise(
n = n(),
share_found = mean(found, na.rm = TRUE),
share_fallback = mean(is_fallback, na.rm = TRUE),
.groups = "drop"
) |>
arrange(desc(share_fallback))
df |>
filter(is_fallback | layer %in% c("region", "country")) |>
select(id, label, layer, match_type, confidence) |>
head(50)
Suggested quality report table
For internal documentation, papers, or appendices, report a compact quality table:
Total input observations
Successfully geocoded observations
Address-level exact matches
Address-level interpolated matches
Street-level matches
Venue-level matches
Locality-level matches
Region-level fallback matches
Country-level fallback matches
Unmatched observations
Final observations retained for main analysis
Final observations excluded or flagged
10. Stata workflows
Many Accounting and Finance researchers work primarily in Stata. The recommended workflow is to geocode in Python or R, export a clean CSV file, and then merge the geocoded output back into Stata. This is usually more robust than parsing nested GeoJSON directly in Stata.
Recommended Stata merge workflow
* 1. Export address data from Stata
use firm_addresses.dta, clear
export delimited original_id firm_name address postalcode locality country ///
using "addresses.csv", replace
* 2. Geocode addresses.csv using the Python or R examples on this page.
* This creates addresses_geocoded.csv.
* 3. Import geocoded output and save as Stata data
import delimited "addresses_geocoded.csv", clear
save "addresses_geocoded.dta", replace
* 4. Merge coordinates back to your research dataset
use firm_panel.dta, clear
merge m:1 original_id using "addresses_geocoded.dta"
* 5. Inspect merge and geocoding quality
tab _merge
tab layer match_type
tab is_fallback
summ confidence
11. Using geocoding results with remote-sensing data
For remote-sensing applications, the required geocoding precision depends on the spatial resolution and research design. A coordinate that is good enough for city-level exposure may be unusable for pixel-level, neighborhood-level, facility-level, or building-level analysis.
| Research use case | Usually acceptable geocoding level | Use with caution / usually exclude |
|---|---|---|
| Building-, plant-, store-, branch-, or facility-level exposure | layer = address, preferably match_type = exact; sometimes interpolated after checks |
street, locality, region, country |
| Neighborhood- or local-environment exposure | address; high-quality street may be acceptable depending on raster resolution |
locality, region, country |
| City-level exposure or city-level controls | address, street, venue, locality |
region, country |
| Regional controls or regional assignment | region may be sufficient if the research design is regional |
country unless country-level analysis is intended |
12. How to interpret result quality
Pelias returns GeoJSON. The most important information is in:
features[0].properties
features[0].geometry.coordinates
Important fields
| Field | Meaning | How to use it |
|---|---|---|
label |
Human-readable result label | Useful for checking whether the match looks plausible. |
layer |
Type of result, e.g. address, street, venue, locality, region, country |
Critical for quality control. Address-level results are usually much more precise than region or country results. |
source |
Underlying data source, e.g. OpenStreetMap, OpenAddresses, GeoNames, Who's On First, or mixed | Useful for diagnostics and reproducibility notes. |
match_type |
Type of match, e.g. exact, interpolated, or fallback |
Exact and interpolated address results are usually more useful than fallback results. |
confidence |
Pelias confidence score | Helpful, but should not be used alone. Combine it with layer and match_type. |
geometry.coordinates |
Coordinates as [longitude, latitude] |
Important: GeoJSON uses longitude first, latitude second. |
Suggested quality categories
| Category | Typical condition | Suggested interpretation |
|---|---|---|
| High-confidence match | layer = address and match_type = exact |
Usually suitable for point-level analysis, subject to sample checks. |
| Useful approximate match | layer = address and match_type = interpolated |
Often useful, but coordinates are estimated along a street segment. |
| Street-level match | layer = street |
Useful when exact house numbers are not available; not a building-level match. |
| Place-level match | layer = locality or localadmin |
Useful for city-level analysis, not address-level analysis. |
| Low-confidence fallback | layer = region or country, especially with match_type = fallback |
Should usually be flagged or excluded from address-level analysis. |
Good vs. bad results
A good address-level result for point-level research may look like this:
{
"label": "Neue Mainzer Strasse 52, Frankfurt, HE, Germany",
"layer": "address",
"match_type": "exact",
"confidence": 1,
"coordinates": [8.67, 50.11]
}
A poor result for address-level research may still be returned by the API:
{
"label": "Hessen, Germany",
"layer": "region",
"match_type": "fallback",
"confidence": 0.6
}
features can still be too coarse for your research design.
13. What works well and what does not
What works well
- Structured addresses with country, city, street, and house number generally work best.
- German and European city-level queries are often strong when names are standardized.
- Address-level matches are usable when Pelias returns
layer = address. - Interpolation works technically where street geometry and address ranges are available. These results have
match_type = interpolated. - Reverse geocoding is useful for checking or enriching existing coordinates.
What requires caution
- Do not treat every result as equally precise. A country or region result is not a successful address geocode.
- Fallback results can look like successful responses. The API may return a result even if it only matched the country, region, or city.
- Company names and institution names are harder than postal addresses. If your input is a firm name rather than a street address, the result may be a venue, city, or unrelated place.
- Historical addresses may be difficult. Street names, boundaries, countries, and city names may have changed over time.
- Non-Latin scripts and transliterations can be difficult. Results for some countries may vary depending on spelling and transliteration.
- PO boxes, headquarters-only addresses, and incomplete address strings often do not produce reliable point-level results.
What we learned from initial testing
In our initial benchmark with 100 firm-address examples, structured search performed better than plain free-text search for research-style address data. However, many weak results were not technical failures. They were coarse fallbacks to regions, countries, or localities.
The most important lesson is that API success is not the same as geocoding success.
A response with features can still be too coarse for analysis.
For this reason, users should always evaluate the returned layer and match_type.
Recommended filtering logic
For address-level research, a simple first-pass filter could look like this:
high_quality =
layer == "address" and
match_type in ["exact", "interpolated"]
medium_quality =
layer in ["street", "venue", "locality"] and
match_type != "fallback"
low_confidence =
layer in ["region", "country"] or
match_type == "fallback"
Depending on your research question, city-level results may still be useful. For example, if your analysis only
requires assigning firms to cities, then layer = locality may be acceptable. If your analysis requires
building-level coordinates, locality-, region-, and country-level results should not be treated as successful matches.
14. Batch jobs and service etiquette
The service can be used for research batch jobs, but please use it responsibly. Large geocoding jobs can run for hours or days and may affect other users if they are started with excessive parallelization.
/v1/search/structured where possible, set size=1,
deduplicate input addresses before sending requests, and start with
2 to 4 parallel requests.
Recommended request rate
For regular research batch jobs, please use conservative parallelization. As a default, run no more than 2 to 4 parallel requests. Avoid aggressive parallelization, unbounded retry loops, or launching many independent jobs at the same time.
Internal tests of the public HTTPS endpoint with API-key authentication reached
around 70 to 75 structured requests per second with moderate
parallelization and size=1. Higher throughput may be possible, but
can lead to stronger latency outliers. These numbers are only a rough orientation
and are not a service guarantee.
Large and very large datasets
For large jobs, especially above 1 million addresses, please contact the SAFE Research Data Center before starting the job. For very large jobs, such as datasets with 10 million addresses or more, coordination with the RDC is required before running the batch.
Panel datasets often contain many repeated addresses. A dataset with 40 million rows may contain far fewer unique addresses. Please create a stable address key, geocode only unique cleaned addresses, and merge the geocoding result back to the full panel.
A dataset with tens of millions of rows should usually not be sent to the API row by row without preprocessing. First normalize and deduplicate address strings, geocode only the unique addresses, and then merge the geocoding results back to the original dataset using a stable address identifier.
Practical batch recommendations
- Start with a small test sample and inspect match quality before running a large job.
- Prefer
/v1/search/structuredfor tabular research data. - Use
size=1if you only need the top result. - Deduplicate addresses before geocoding.
- Store results locally instead of repeatedly geocoding the same addresses.
- Use 2 to 4 parallel requests as the normal default.
- Avoid aggressive parallelization from R, Python, Stata, notebooks, or compute clusters.
- Do not use unbounded retry loops. Failed requests should be logged and retried later with limits.
- For jobs above 1 million addresses, contact the SAFE Research Data Center first.
- For jobs above 10 million addresses, coordinate with the RDC before starting.
15. Data sources and limitations
This service is based on Pelias and open geospatial data sources. The current instance uses data from sources including OpenStreetMap, OpenAddresses, Who's On First, GeoNames, and related Pelias components.
Coverage and quality depend on the underlying open data. Some countries and cities have excellent address-level coverage. Others have sparse address data, inconsistent formatting, or limited house-number coverage.
Important limitations
- The service is not a legal address registry.
- The service does not guarantee building-level accuracy.
- Coordinates are based on current open geospatial data and may not represent historical locations.
- Returned coordinates should be validated for research projects, especially for non-European, historical, or incomplete addresses.
- For sensitive research outputs, document your geocoding method and filtering criteria.
Update schedule and data vintage
The service is not updated continuously. Data updates are performed by the SAFE Research Data Center as maintenance tasks. For reproducibility, always record the date of geocoding. If your project depends on a specific data vintage, please contact the RDC.
Data protection and confidential data
Researchers remain responsible for complying with data protection rules, licensing restrictions, and project-specific confidentiality requirements. Do not submit confidential personal data unless your project has clarified that this is permitted.
The service is operated for research support. Full geocoding query strings are not used for routine usage statistics. However, researchers remain responsible for ensuring that their use of the service is compatible with data protection, confidentiality, and project-specific restrictions.
16. Reproducibility and citation notes
For papers, replication packages, and internal project documentation, record how geocoding was performed. At minimum, document:
- Service used: SAFE Geocoding Service, based on Pelias.
- Date of geocoding.
- Endpoint used, e.g.
/v1/search/structured. - Input fields used, e.g. address, postal code, locality, country.
- Whether
size=1or multiple candidates were requested. - Filtering rules for low-confidence matches.
- Whether country-, region-, locality-, or fallback-level results were excluded.
- Any manual validation or correction steps.
Example methods note:
Addresses were geocoded using the SAFE Geocoding Service, an internal Pelias-based
geocoder using open geospatial data. We used the structured search endpoint with
street address, locality, postal code, and country fields where available. For batch
geocoding, we requested the top candidate only and stored the returned label, layer,
match type, confidence, source, global identifier, and coordinates. We retained
address-level exact and interpolated matches for point-level analyses and flagged
region-, country-, and fallback-level results as low-confidence. The final analysis
sample excludes observations whose geocoding precision is insufficient for the spatial
resolution of the remote-sensing data used in the study.
17. Official Pelias documentation
The links below describe the general Pelias API. The SAFE instance may differ in data coverage, configuration, access control, and update schedule.
Pelias API Version: 1.0,
this refers to the Pelias API/engine interface version. It is not necessarily the same as the installed
pelias/api software release version.
- Pelias project website
- Official Pelias documentation repository
- Pelias API server repository
- Structured geocoding documentation
- Search endpoint documentation
- Reverse geocoding documentation
- Autocomplete documentation
18. FAQ
Why did I get a result even though the address was incomplete or wrong?
Geocoders often return the best available candidate. If the exact address cannot be found,
the result may fall back to a street, city, region, or country. This is why you must inspect
layer and match_type.
Can I use all returned coordinates in a remote-sensing analysis?
No. For fine-grained remote-sensing applications, use only coordinates with sufficient spatial precision for your raster or exposure measure. Region-, country-, and fallback-level results should not be treated as point locations.
Why are coordinates returned as longitude, latitude?
Pelias returns GeoJSON. GeoJSON coordinates are ordered as [longitude, latitude].
This is the opposite of how coordinates are often spoken or written informally.
Should I save only latitude and longitude?
No. Always save diagnostic fields such as label, layer,
match_type, confidence, source, and gid.
Otherwise you cannot later distinguish precise address matches from coarse fallback matches.
Why does a company address sometimes return a venue or unrelated place?
Firm names, legal entities, headquarters, branches, and facilities are not the same type of information as postal addresses. If possible, geocode street addresses rather than company names.
What should I do before running a very large batch job?
Start with a small sample, inspect match quality, and contact the SAFE Research Data Center if you plan to geocode a very large dataset. Avoid aggressive parallel requests.
19. Contact
This is an experimental research infrastructure service operated by the SAFE Research Data Center. For access, API keys, larger batch jobs, or methodological questions, please contact datacenter@safe-frankfurt.de.
geocode.safe-frankfurt.de. The public API currently runs on the pinned Docker image
pelias/api:v7.8.0. If the attribution page or API metadata displays
Pelias API Version: 1.0, this refers to the Pelias API/engine interface version,
not to the Docker image tag. For research publications, please document the date of geocoding,
endpoint used, filtering criteria, and whether coarse fallback results were included or excluded.