# Add coordinates to a CSV file

> Geocode a list of addresses to x/y in WGS84 or UTM32 with datavask and id lookups, in plain Python or with pandas.
>
> https://danadresse.dk/en/guides/geokod-csv · verified 2026-11-02
A map of customers, a route plan or an analysis by area needs coordinates on every address. If you have a CSV with street name, house number and postcode, you can add the columns `x` and `y` with two kinds of calls: datavask finds the address id, and a lookup by id gives the coordinates.

The coordinates belong to the access address (adgangsadresse), the entrance of the building. Every flat on the same staircase shares the same point, so you can geocode at building level without floor and door.

## 1. Find the access address with datavask

`GET /datavask/adgangsadresser` takes street name, house number and postcode (or the whole address in `betegnelse`) and returns the best match with its id:

```bash id=geo-curl run=live expect="adgangsadresser"
curl -s -G "https://api.danadresse.dk/datavask/adgangsadresser" \
  --data-urlencode "vejnavn=Strandvejen" \
  --data-urlencode "husnr=100" \
  --data-urlencode "postnr=2900" \
  -H "X-Api-Key: $DANADRESSE_API_KEY"
```

In the response, `resultater[0].adresse.id` is the access-address id. The response has the same shape as DAWA's datavask and contains no coordinates. Only take rows with `kategori` A or B. A C row gets empty coordinates so that a person can look at it.

## 2. Fetch the coordinates by id

`GET /adgangsadresser` takes several ids separated by `|`. With `struktur=mini` you get a flat object with `x` and `y`. Without `srid` they are longitude and latitude in WGS84 with eight decimals. With `srid=25832` they are in UTM32 metres, which is handy for distances and GIS:

```bash id=geo-ids run=live expect="6175755"
curl -s -G "https://api.danadresse.dk/adgangsadresser" \
  --data-urlencode "id=0a3f507c-0624-32b8-e044-0003ba298018|0a3f507a-ec01-32b8-e044-0003ba298018" \
  --data-urlencode "struktur=mini" \
  --data-urlencode "srid=25832" \
  -H "X-Api-Key: $DANADRESSE_API_KEY"
```

The response does not come back in the order of the ids. Match on `id`, not on position. Keep to about 100 ids per call so the URL does not get too long.

To get both coordinate systems in one call, add `format=csv`. The CSV response always has the columns `wgs84koordinat_længde`, `wgs84koordinat_bredde`, `etrs89koordinat_øst` and `etrs89koordinat_nord`.

## 3. The whole flow with the csv module

The example reads two rows from a string, washes them and fetches the coordinates in WGS84 and UTM32 with one call per coordinate system. It only uses `requests` and the standard library:

```python id=geo-csv run=live expect="x_utm"
import csv
import io
import os
import sys
import requests

DATA = """kundenr,vejnavn,husnr,postnr
1001,Strandvejen,100,2900
1002,Rådhuspladsen,1,1550
"""

s = requests.Session()
s.headers["X-Api-Key"] = os.environ["DANADRESSE_API_KEY"]
API = "https://api.danadresse.dk"

rows = list(csv.DictReader(io.StringIO(DATA)))
for row in rows:
    r = s.get(f"{API}/datavask/adgangsadresser", timeout=10, params={
        "vejnavn": row["vejnavn"], "husnr": row["husnr"], "postnr": row["postnr"]})
    r.raise_for_status()
    svar = r.json()
    ok = svar["kategori"] in ("A", "B") and svar["resultater"]
    row["kategori"] = svar["kategori"]
    row["adgangsadresseid"] = svar["resultater"][0]["adresse"]["id"] if ok else ""

ids = [r["adgangsadresseid"] for r in rows if r["adgangsadresseid"]]

def punkter(srid):
    r = s.get(f"{API}/adgangsadresser", timeout=10, params={
        "id": "|".join(ids), "struktur": "mini", "srid": srid, "per_side": len(ids)})
    r.raise_for_status()
    return {a["id"]: (a["x"], a["y"]) for a in r.json()}

wgs84, utm = punkter(4326), punkter(25832)
for row in rows:
    aid = row["adgangsadresseid"]
    row["lon"], row["lat"] = wgs84.get(aid, ("", ""))
    row["x_utm"], row["y_utm"] = utm.get(aid, ("", ""))

out = csv.DictWriter(sys.stdout, fieldnames=list(rows[0].keys()))
out.writeheader()
out.writerows(rows)
```

For a real file, replace `io.StringIO(DATA)` with `open("kunder.csv", encoding="utf-8-sig")`, split the id list into chunks of 100 and handle `429` as described in [Limits, quotas and 429](/en/guides/graenser-og-429).

## 4. The same with pandas

With pandas it is handy to wash each unique address once, fetch the coordinates in chunks of 100 ids and merge the result back. Many CRM exports have the same address on several customers:

```python id=geo-pandas run=syntax
import os
import pandas as pd
import requests

s = requests.Session()
s.headers["X-Api-Key"] = os.environ["DANADRESSE_API_KEY"]

df = pd.read_csv("kunder.csv", dtype=str, encoding="utf-8-sig")
unikke = df[["vejnavn", "husnr", "postnr"]].drop_duplicates()

def vask(r):
    svar = s.get("https://api.danadresse.dk/datavask/adgangsadresser", timeout=10,
                 params={"vejnavn": r.vejnavn, "husnr": r.husnr, "postnr": r.postnr}).json()
    ok = svar["kategori"] in ("A", "B") and svar["resultater"]
    return pd.Series({"kategori": svar["kategori"],
                      "adgangsadresseid": svar["resultater"][0]["adresse"]["id"] if ok else None})

unikke = unikke.join(unikke.apply(vask, axis=1))

ids = unikke["adgangsadresseid"].dropna().unique().tolist()
punkt = {}
for i in range(0, len(ids), 100):
    bunke = ids[i:i + 100]
    r = s.get("https://api.danadresse.dk/adgangsadresser", timeout=30, params={
        "id": "|".join(bunke), "struktur": "mini", "per_side": len(bunke)})
    r.raise_for_status()
    punkt.update({a["id"]: (a["x"], a["y"]) for a in r.json()})
unikke["x"] = unikke["adgangsadresseid"].map(lambda i: punkt.get(i, (None, None))[0])
unikke["y"] = unikke["adgangsadresseid"].map(lambda i: punkt.get(i, (None, None))[1])
df = df.merge(unikke, on=["vejnavn", "husnr", "postnr"], how="left")
df.to_csv("kunder_geokodet.csv", index=False)
print(df["kategori"].value_counts())
```

Read every column as text (`dtype=str`). Otherwise codes with a leading zero, such as the municipality code `0101`, lose the zero, and house numbers like `12A` get mixed with numbers.

## Which coordinate system?

| `srid` | System | Unit | Use |
|---|---|---|---|
| `4326` (default) | WGS84 | degrees (longitude, latitude) | Web maps, GPS, Google Maps, Leaflet |
| `25832` | ETRS89 / UTM zone 32N | metres (east, north) | Distances, GIS, Danish map data |

Mind the order: `x` is longitude and `y` is latitude. Many tools want `lat, lon`, so swap them when you write to those. See [Coordinates and srid](/en/guides/koordinater-srid).

## Edge cases

- **No coordinates.** A few addresses have no access point. Then `x` and `y` are `null`.
- **Accuracy.** The full response from `/adgangsadresser/{id}` has `adgangspunkt.nøjagtighed`: `A` is measured, `B` is calculated, `U` is uncertain. Filter on it if precision matters to you.
- **Semicolon files.** Danish Excel exports often use semicolons. Use `csv.DictReader(f, delimiter=";")` or `pd.read_csv(..., sep=";")`.
- **Ids you already have.** If you have address ids from an earlier wash, skip datavask and fetch the coordinates directly with `GET /adresser?id=a|b|c&struktur=mini`.

## Next steps

- [Addresses in a radius or a polygon](/en/guides/adresser-i-radius-og-polygon)
- [Clean a CSV export from your CRM](/en/guides/rens-crm-csv)
- More about geocoding: [geocoding](/geokodning)
