This service provides geocoding infrastructure for research purposes at SAFE. It is based on Pelias, an open-source geocoder using open geospatial data.

What can I use this for? This service helps researchers convert postal addresses into geographic coordinates for empirical research. Typical use cases include linking firms, banks, branches, plants, facilities, or other entities to regions, neighborhoods, distances, climate and pollution measures, remote-sensing data, or other spatial datasets. The service is intended for research workflows, not for address verification, delivery logistics, or legal address validation.
Access: The API endpoints under /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.
If you only read one thing: For most research datasets, use /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.
Default recommendation for research datasets: If your data are tabular and contain separate address fields, use /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.

Important: GeoJSON coordinates are ordered as [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
Recommended default: Use /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

  1. Prefer structured search if your data has separate address fields.
  2. Use country information where available, preferably as a two-letter ISO code such as DE, US, GB, FR.
  3. In output, Pelias may return three-letter country abbreviations such as DEU.
  4. Do not blindly trust the first result. Always inspect layer, match_type, confidence, and the coordinates.
  5. Treat country-level and region-level fallback results as low-confidence results.
  6. For large research projects, manually inspect a sample of matches before using the coordinates in analysis.
  7. For batch geocoding, consider setting size=1 if 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.

Practical rule: Decide first what spatial unit your analysis really needs. Then decide which Pelias result layers are acceptable. Do not make this decision after seeing convenient coordinates.

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
Tip: For empirical projects, keep both 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

  1. Tabulate results by found, layer, and match_type.
  2. Check geocoding success rates by country, year, data source, and relevant sample groups.
  3. Inspect all region, country, and fallback results before using them.
  4. Manually review a random sample of high-, medium-, and low-quality matches.
  5. Check whether returned countries match the input countries.
  6. For remote-sensing applications, map a sample of points and compare them to the expected locations.
  7. 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
Best practice for Stata users: Keep geocoding as a separate, reproducible data-preparation step. Save the geocoded output with diagnostic fields and merge it into Stata only after quality checks.

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
Remote-sensing warning: Do not merge region-, country-, or fallback-level coordinates into fine-grained raster data as if they were point-level locations. This can create severe measurement error.

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
}
Key lesson: API success is not the same as geocoding success. A response with 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.

Recommended default for batch geocoding: Use /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

Very large jobs: Please do not start multi-million-address batch jobs without coordination. At 40 million addresses, even a fast and stable run can take several days once preprocessing, retries, validation, and possible restarts are taken into account.
For large jobs, please coordinate with the RDC before increasing request rates, removing delays, or running parallel jobs.

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

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:

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.

Version note: If the API or attribution page displays 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.

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.

Version note: This documentation describes the SAFE Pelias instance at 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.