# Hjemmebygget DAWA-autocomplete med fetch eller jQuery

> Flyt et selvskrevet adressefelt fra DAWA's /autocomplete til Danadresse – URL, nøgle, JSONP eller JSON, fejlhåndtering og caretpos-løkken.
>
> https://danadresse.dk/guides/migrer-egen-dawa-autocomplete · verified 2026-11-02
Mange byggede deres eget adressefelt oven på DAWA's `/autocomplete` med `fetch`, `XMLHttpRequest` eller jQuery. Den slags kode virker igen med få ændringer, fordi svaret har samme form: en liste af forslag med `type`, `tekst`, `forslagstekst`, `caretpos` og `data`.

Guiden gennemgår de fem ting, du skal ændre, og viser et komplet eksempel.

## 1. Skift URL'en og tilføj en browsernøgle

Typisk DAWA-kode så sådan ud:

```js id=before-jquery run=no
$("#adresse").on("input", function () {
  $.ajax({
    url: "https://dawa.aws.dk/autocomplete",
    dataType: "jsonp",
    data: { q: this.value, caretpos: this.selectionStart, type: "adresse" },
    success: visForslag
  });
});
```

Domænet bliver `https://api.danadresse.dk`, og kaldet skal have en nøgle. Koden kører i browseren, så nøglen er synlig for alle. Brug en nøgle, der er låst til dine domæner under **Tilladte domæner** i [Nøgler](/dashboard/keys) – se [Browsernøgler og tilladte domæner](/guides/browser-noegler-domaener).

Send nøglen som `api_key` i URL'en. En `X-Api-Key`-header virker også, men en egen header får browseren til at sende en ekstra CORS-forespørgsel (`OPTIONS`) før kaldet, og det gør den for hver ny URL – altså for hvert tastetryk. Med `api_key` i URL'en slipper du for den ekstra rundtur.

## 2. Brug hellere JSON end JSONP

JSONP virker som i DAWA: med `callback=fn` bliver svaret pakket ind som `fn(…)` (`application/javascript`), så jQuery's `dataType: "jsonp"` virker videre, når nøglen er tilføjet. Men med JSONP kører browseren ikke scriptet, når kaldet fejler, så din kode ser aldrig status eller fejlens `code`. CORS er åbent for alle domæner, så skift til `dataType: "json"` eller `fetch`, når du alligevel retter i koden.

## 3. Håndter fejl

DAWA svarede næsten altid 200. Hos os kan et kald fejle af grunde, der handler om nøglen og forbruget. Fejlsvar på DAWA-stierne har DAWA's form `{"type", "title", "details", "code"}`. Styr efter `code`:

| Status | `code` | Hvad du gør |
|---|---|---|
| 401 | `missing_api_key`, `invalid_api_key` | Vis ingen forslag og log fejlen. Det er en konfigurationsfejl, så prøv ikke igen |
| 403 | `origin_not_allowed` | Sidens domæne mangler på nøglens liste. Prøv ikke igen |
| 403 | `key_blocked` | Nøglen er spærret som lækket. Lås den til dine domæner eller lav en ny |
| 429 | `rate_limit_per_visitor`, `rate_limit_per_minute` | Hold pause et par sekunder |
| 429 | `quota_exceeded` | Månedskvoten på gratisplanen er brugt. Prøv ikke igen før næste måned |

De minutbaserede 429-svar har `Retry-After` med antal sekunder, du skal vente (1 til 60), og browseren lader dit script læse den. Hold pause så længe. `quota_exceeded` har ingen `Retry-After`; hold op med at spørge, til brugeren genindlæser siden. Et adressefelt, der holder op med at foreslå et øjeblik, er bedre end et, der bliver ved med at ramme grænsen.

## 4. Forstå caretpos-løkken

DAWA's autocomplete arbejder i tre trin: vejnavn, adgangsadresse (husnummer) og adresse (etage og dør). Når brugeren vælger et forslag, der ikke er det endelige niveau, skal du:

1. sætte feltets tekst til forslagets `tekst`,
2. sætte markøren på `caretpos`,
3. spørge igen med den nye tekst og `caretpos`.

Det fortsætter, til brugeren vælger et forslag af den type, du vil ende med. For et adgangsadresse-forslag er `tekst` fx `"Vesterbrogade 145, , 1620 København V"` med markøren lige efter `"145, "`, så brugeren kan skrive etage og dør i det tomme felt. Brug `forslagstekst` til at vise forslaget i listen og `tekst` til feltet.

Én forskel fra DAWA: sender du `type=adresse`, returnerer vi kun forslag på adresseniveau, også mens brugeren stadig skriver vejnavnet. DAWA viste vejnavne først. Uden `type` får du de tre trin som i DAWA. Skriv løkken, så den virker i begge tilfælde: vælger brugeren et forslag med den ønskede type, er du færdig. Filtrene `postnr`, `kommunekode` og `adgangsadresseid` indsnævrer forslagene som i DAWA, og tolerancen over for stavefejl er et flag som i DAWA: slået til med `fuzzy` (eller `fuzzy=`), slået fra uden den eller med `fuzzy=false`.

Løkken kan du prøve fra Node. Den starter med `"Rådhuspladsen 1"`, vælger første forslag og borer videre, indtil den rammer en adresse:

```js id=drill-loop run=live expect="Rådhuspladsen"
const API = "https://api.danadresse.dk/autocomplete";
const headers = { "X-Api-Key": process.env.DANADRESSE_API_KEY };

let tekst = "Rådhuspladsen 1";
let caretpos = tekst.length;

for (let trin = 1; trin <= 3; trin++) {
  const url = new URL(API);
  url.search = new URLSearchParams({ q: tekst, caretpos: String(caretpos), per_side: "5" });
  const res = await fetch(url, { headers });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  const forslag = await res.json();
  if (forslag.length === 0) break;

  const valgt = forslag[0];
  console.log(`trin ${trin}: ${valgt.type} -> "${valgt.tekst}" (caretpos ${valgt.caretpos})`);
  if (valgt.type === "adresse") {
    console.log(`færdig, adresse-id ${valgt.data.id}`);
    break;
  }
  tekst = valgt.tekst;
  caretpos = valgt.caretpos;
}
```

## 5. Et komplet felt med fetch

Eksemplet venter 200 ms efter sidste tastetryk (debounce), afbryder et kald, der er blevet forældet, og håndterer fejlene fra trin 3. Det bruger kun browserens egne API'er:

```js id=fetch-field run=syntax
const API = "https://api.danadresse.dk/autocomplete";
const API_KEY = "dawa_live_din_browsernøgle"; // domænelåst browsernøgle
const MAAL = "adresse"; // eller "adgangsadresse", hvis du ikke vil have etage og dør

const input = document.querySelector("#adresse");
const liste = document.querySelector("#forslag");
let controller = null;
let timer = null;
let pauseTil = 0;

async function hentForslag(tekst, caretpos) {
  if (Date.now() < pauseTil || tekst.trim().length < 2) return [];
  controller?.abort();
  controller = new AbortController();
  const url = new URL(API);
  url.search = new URLSearchParams({
    q: tekst,
    caretpos: String(caretpos),
    per_side: "10",
    api_key: API_KEY,
  });
  let res;
  try {
    res = await fetch(url, { signal: controller.signal });
  } catch (err) {
    if (err.name === "AbortError") return null; // et nyere tastetryk har overtaget
    throw err;
  }
  if (res.status === 429) {
    const fejl = await res.json();
    // quota_exceeded: måneden er brugt op, stop. Ellers vent Retry-After sekunder.
    const vent = fejl.code === "quota_exceeded" ? 3600 : Number(res.headers.get("Retry-After")) || 5;
    pauseTil = Date.now() + vent * 1000;
    return [];
  }
  if (res.status === 401 || res.status === 403) {
    const fejl = await res.json();
    console.error("Danadresse:", fejl.code, fejl.title);
    return [];
  }
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

function visForslag(forslag) {
  liste.replaceChildren(...forslag.map((f) => {
    const li = document.createElement("li");
    li.textContent = f.forslagstekst;
    li.addEventListener("mousedown", (e) => {
      e.preventDefault();
      vaelg(f);
    });
    return li;
  }));
}

async function opdater(tekst, caretpos) {
  const forslag = await hentForslag(tekst, caretpos);
  if (forslag) visForslag(forslag);
}

function vaelg(f) {
  input.value = f.tekst;
  input.setSelectionRange(f.caretpos, f.caretpos);
  if (f.type === MAAL) {
    liste.replaceChildren();
    input.dataset.adresseId = f.data.id; // gem id'et, ikke kun teksten
    return;
  }
  opdater(f.tekst, f.caretpos); // vejnavn eller adgangsadresse: bor videre
}

input.addEventListener("input", () => {
  delete input.dataset.adresseId;
  clearTimeout(timer);
  timer = setTimeout(() => opdater(input.value, input.selectionStart), 200);
});
```

Med jQuery er ændringen mindre. Skift URL, `dataType` og tilføj nøglen og en fejlhandler:

```js id=after-jquery run=no
$("#adresse").on("input", function () {
  $.ajax({
    url: "https://api.danadresse.dk/autocomplete",
    dataType: "json",
    data: { q: this.value, caretpos: this.selectionStart, api_key: API_KEY },
    success: visForslag,
    error: function (xhr) {
      var fejl = xhr.responseJSON || {};
      console.error("Danadresse:", xhr.status, fejl.code);
    }
  });
});
```

Husk også debounce her. Uden den bruger hvert tastetryk et kald af din kvote.

## Næste skridt

- Vil du hellere bruge en færdig komponent? [Autocomplete-widget i ren JavaScript](/guides/vanilla-autocomplete-widget).
- Hvad betyder grænserne i praksis? [Grænser, kvoter og 429](/guides/graenser-og-429).
- Gem det valgte id, ikke kun teksten: [Gem adresse-id'et](/guides/gem-adresse-id).
