Hand-built DAWA autocomplete with fetch or jQuery
Move a self-written address field from DAWA's /autocomplete to Danadresse – URL, key, JSONP or JSON, error handling and the caretpos loop.
Many teams built their own address field on top of DAWA's /autocomplete with fetch, XMLHttpRequest or jQuery. That kind of code works again with a few changes, because the response has the same shape: a list of suggestions with type, tekst, forslagstekst, caretpos and data.
This guide goes through the five things to change and shows a complete example.
1. Change the URL and add a browser key
Typical DAWA code looked like this:
$("#adresse").on("input", function () { $.ajax({ url: "https://dawa.aws.dk/autocomplete", dataType: "jsonp", data: { q: this.value, caretpos: this.selectionStart, type: "adresse" }, success: showSuggestions }); });
The domain becomes https://api.danadresse.dk, and the call needs a key. The code runs in the browser, so the key is visible to everyone. Use a key that is locked to your domains under Tilladte domæner (allowed domains) in Keys – see Browser keys and allowed domains.
Send the key as api_key in the URL. An X-Api-Key header works too, but a custom header makes the browser send an extra CORS request (OPTIONS) before the call, and it does so for every new URL – that is, for every keystroke. With api_key in the URL you avoid that extra round trip.
2. Prefer JSON over JSONP
JSONP works as in DAWA: with callback=fn the response is wrapped as fn(…) (application/javascript), so jQuery's dataType: "jsonp" keeps working once the key is added. But with JSONP the browser does not run the script when the call fails, so your code never sees the status or the error code. CORS is open to every domain, so switch to dataType: "json" or fetch when you touch the code anyway.
3. Handle errors
DAWA almost always answered 200. With us a call can fail for reasons that concern the key and usage. Error responses on the DAWA paths have DAWA's shape {"type", "title", "details", "code"}. Branch on code:
| Status | code | What to do |
|---|---|---|
| 401 | missing_api_key, invalid_api_key | Show no suggestions and log the error. It is a configuration error, so do not retry |
| 403 | origin_not_allowed | The page's domain is missing from the key's list. Do not retry |
| 403 | key_blocked | The key was blocked as leaked. Lock it to your domains or create a new one |
| 429 | rate_limit_per_visitor, rate_limit_per_minute | Pause for a few seconds |
| 429 | quota_exceeded | The free plan's monthly quota is used up. Do not retry before next month |
The per-minute 429 responses carry Retry-After with the number of seconds to wait (1 to 60), and the browser lets your script read it. Pause for that long. quota_exceeded has no Retry-After; stop asking until the user reloads the page. An address field that stops suggesting for a moment is better than one that keeps hitting the limit.
4. Understand the caretpos loop
DAWA's autocomplete works in three steps: street name, adgangsadresse (house number) and adresse (floor and door). When the user picks a suggestion that is not the final level, you:
- set the field's text to the suggestion's
tekst, - put the cursor at
caretpos, - ask again with the new text and
caretpos.
This continues until the user picks a suggestion of the type you want to end with. For an adgangsadresse suggestion, tekst is e.g. "Vesterbrogade 145, , 1620 København V" with the cursor right after "145, ", so the user can type floor and door into the empty slot. Use forslagstekst to show the suggestion in the list and tekst for the field.
One difference from DAWA: if you send type=adresse, we return only address-level suggestions, even while the user is still typing the street name. DAWA showed street names first. Without type you get the three steps as in DAWA. Write the loop so it works either way: when the user picks a suggestion of the wanted type, you are done. The filters postnr, kommunekode and adgangsadresseid narrow the suggestions as in DAWA, and typo tolerance is a flag as in DAWA: on with fuzzy (or fuzzy=), off without it or with fuzzy=false.
You can try the loop from Node. It starts with "Rådhuspladsen 1", picks the first suggestion and drills down until it reaches an address:
const API = "https://api.danadresse.dk/autocomplete"; const headers = { "X-Api-Key": process.env.DANADRESSE_API_KEY }; let text = "Rådhuspladsen 1"; let caretpos = text.length; for (let step = 1; step <= 3; step++) { const url = new URL(API); url.search = new URLSearchParams({ q: text, 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 suggestions = await res.json(); if (suggestions.length === 0) break; const picked = suggestions[0]; console.log(`step ${step}: ${picked.type} -> "${picked.tekst}" (caretpos ${picked.caretpos})`); if (picked.type === "adresse") { console.log(`done, address id ${picked.data.id}`); break; } text = picked.tekst; caretpos = picked.caretpos; }
5. A complete field with fetch
The example waits 200 ms after the last keystroke (debounce), cancels a call that has gone stale and handles the errors from step 3. It uses only the browser's own APIs:
const API = "https://api.danadresse.dk/autocomplete"; const API_KEY = "dawa_live_your_browser_key"; // domain-locked browser key const TARGET = "adresse"; // or "adgangsadresse" if you do not want floor and door const input = document.querySelector("#adresse"); const list = document.querySelector("#suggestions"); let controller = null; let timer = null; let pauseUntil = 0; async function fetchSuggestions(text, caretpos) { if (Date.now() < pauseUntil || text.trim().length < 2) return []; controller?.abort(); controller = new AbortController(); const url = new URL(API); url.search = new URLSearchParams({ q: text, 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; // a newer keystroke took over throw err; } if (res.status === 429) { const error = await res.json(); // quota_exceeded: the month is used up, stop asking. Otherwise wait Retry-After seconds. const wait = error.code === "quota_exceeded" ? 3600 : Number(res.headers.get("Retry-After")) || 5; pauseUntil = Date.now() + wait * 1000; return []; } if (res.status === 401 || res.status === 403) { const error = await res.json(); console.error("Danadresse:", error.code, error.title); return []; } if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } function showSuggestions(suggestions) { list.replaceChildren(...suggestions.map((s) => { const li = document.createElement("li"); li.textContent = s.forslagstekst; li.addEventListener("mousedown", (e) => { e.preventDefault(); pick(s); }); return li; })); } async function refresh(text, caretpos) { const suggestions = await fetchSuggestions(text, caretpos); if (suggestions) showSuggestions(suggestions); } function pick(s) { input.value = s.tekst; input.setSelectionRange(s.caretpos, s.caretpos); if (s.type === TARGET) { list.replaceChildren(); input.dataset.addressId = s.data.id; // store the id, not just the text return; } refresh(s.tekst, s.caretpos); // street name or adgangsadresse: drill down } input.addEventListener("input", () => { delete input.dataset.addressId; clearTimeout(timer); timer = setTimeout(() => refresh(input.value, input.selectionStart), 200); });
With jQuery the change is smaller. Change the URL and dataType, and add the key and an error handler:
$("#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: showSuggestions, error: function (xhr) { var error = xhr.responseJSON || {}; console.error("Danadresse:", xhr.status, error.code); } }); });
Remember debounce here too. Without it every keystroke uses a call from your quota.
Next steps
- Prefer a ready-made component? Autocomplete widget in plain JavaScript.
- What do the limits mean in practice? Limits, quotas and 429.
- Store the chosen id, not just the text: Store the address id.
Related guides
Markdown source Spotted an error? Tell us API-reference (Swagger)