Replacing getAddress.io? Free drop-in replacement →
← Back to Blog
Tutorials Dec 8, 2025 · 8 min read

Address Lookup Tutorial: Typeahead → Resolve → Enriched Property in Under 50 Lines

Build an address autocomplete with property enrichment using the Homedata API. From typeahead to bedrooms, EPC score, and comparable sales — in under 50 lines of Python or JavaScript.

What you're building

An address autocomplete that doesn't just return a formatted string — it returns a property. Type a street name, pick an address, and your form fills with bedrooms, property type, EPC score, floor area, and the last sold price. No manual lookups. No secondary API calls. One resolved UPRN unlocks it all.

This tutorial covers three steps:

  1. Find — instant typeahead across 36 million UK addresses (2 calls, no API key needed)
  2. Retrieve — resolve a UPRN to enriched property data (API key required)
  3. Chain — use the UPRN to pull EPC data and comparable sales

The full working implementation is under 50 lines of Python. The JavaScript widget is about the same. Both are below.

You'll need:

  • A free Homedata API key — 100 requests/month on the free tier, no card required
  • Python 3.8+ (for the server-side examples)
  • pip install requests

Step 1 — Find: address typeahead (2 calls, no key needed)

The Find endpoint is the entry point. It queries a 36-million-address Elasticsearch index and returns suggestions in under 50ms. No API key required for the public endpoint. This is deliberate — you want your developers to start integrating before they've even signed up.

Pass a q parameter with at least 2 characters. It handles postcodes, street names, building numbers — all three query modes auto-detect from the input.

# No API key required
curl "https://api.homedata.co.uk/api/address/find/?q=10+downing+street"
{
  "suggestions": [
    {
      "uprn": 100023336956,
      "address": "10 DOWNING STREET, LONDON, SW1A 2AA",
      "postcode": "SW1A 2AA",
      "town": "London"
    },
    {
      "uprn": 10008315461,
      "address": "10A DOWNING STREET, LONDON, SW1A 2AB",
      "postcode": "SW1A 2AB",
      "town": "London"
    }
  ],
  "count": 2
}

The key field is uprn. That's the Unique Property Reference Number — your handle to everything that follows. Store it. Pass it to Retrieve.

You can also filter results to a specific postcode with ?postcode=SW1A, or increase the result count up to 15 with ?limit=10.

Alternative: list all addresses at a postcode

If you already know the postcode — batch processing a dataset, or building a postcode-first lookup flow — use the Postcode endpoint instead. It returns every address at that postcode with full building identifiers. No auth required.

curl "https://api.homedata.co.uk/api/address/postcode/SW1A2AA/"

# Response
{
  "postcode": "SW1A 2AA",
  "count": 3,
  "addresses": [
    {
      "uprn": 100023336956,
      "address": "10 DOWNING STREET, LONDON, SW1A 2AA",
      "building_name": "",
      "building_number": "10",
      "sub_building": "",
      "street": "Downing Street",
      "town": "London"
    }
  ]
}

Each address includes building_number and building_name — useful if you need to chain to an endpoint that requires a building identifier rather than a UPRN.


Step 2 — Retrieve: resolve a UPRN to a full property record

Retrieve is where the value is. Pass a UPRN and an enrichment level, and you get back the property record. There are three levels:

  • address (default) — formatted address + coordinates
  • property — adds bedrooms, property type, EPC score, floor area, last sold price
  • full — adds predicted value, construction details, structural data, area stats, 55+ fields total

For most form-filling and qualification use cases, level=property is the right call. It's the balance point between data richness and response speed.

curl "https://api.homedata.co.uk/api/address/retrieve/100023336956/?level=property" \
  -H "Authorization: Api-Key YOUR_API_KEY"
{
  "uprn": 100023336956,
  "full_address": "10 DOWNING STREET, LONDON, SW1A 2AA",
  "address_line_1": "10 Downing Street",
  "address_line_2": "",
  "postcode": "SW1A 2AA",
  "latitude": 51.5034,
  "longitude": -0.1276,
  "level": "property",

  "property_type": "Terraced",
  "bedrooms": 4,
  "bathrooms": 3,
  "epc_floor_area": 248,
  "current_energy_efficiency": 71,
  "potential_energy_efficiency": 83,
  "last_epc_date": "2019-06-14",
  "last_sold_date": "2010-01-12",
  "last_sold_price": 825000
}

With one API call you know: the property is a 4-bed terraced house, EPC Band C (score 71), 248m² floor area, last sold for £825k in 2010. That's a mortgage application pre-filled. Or a lettings listing scaffolded. Or a valuation context built.

The current_energy_efficiency score maps to letter bands: 1–20 = G, 21–38 = F, 39–54 = E, 55–68 = D, 69–80 = C, 81–91 = B, 92–100 = A. You don't need to call the EPC endpoint separately if you just want the band — Retrieve gives you the score and you derive the band yourself.


Step 3 — Chain: EPC details and comparable sales

The UPRN from Find is the key to every other endpoint. Here's where the real power shows up: once you have it, a single lookup triggers a cascade of property intelligence.

For an EPC deep-dive — construction age, floor area from the official certificate, EPC id — call the EPC endpoint separately. The code below chains all three: Find → Retrieve → Comparables → EPC, with the same UPRN threading through each step.

import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.homedata.co.uk"
HEADERS = {"Authorization": f"Api-Key {API_KEY}"}

# Step 1: Find the address (no key needed)
suggestions = requests.get(
    f"{BASE}/api/address/find/",
    params={"q": "42 acacia avenue, leeds", "limit": 5}
).json()["suggestions"]

if not suggestions:
    raise SystemExit("No results found")

# Pick the first match
uprn = suggestions[0]["uprn"]
address = suggestions[0]["address"]
print(f"Resolved: {address} (UPRN {uprn})")

# Step 2: Enrich the property (key required)
prop = requests.get(
    f"{BASE}/api/address/retrieve/{uprn}/",
    params={"level": "property"},
    headers=HEADERS
).json()

print(f"  Type: {prop.get('property_type')} | Beds: {prop.get('bedrooms')}")
print(f"  Floor area: {prop.get('epc_floor_area')}m²")
print(f"  EPC score: {prop.get('current_energy_efficiency')} (potential: {prop.get('potential_energy_efficiency')})")
print(f"  Last sold: £{prop.get('last_sold_price'):,} on {prop.get('last_sold_date')}")

# Step 3: Pull comparable sales
comps = requests.get(
    f"{BASE}/api/comparables/{uprn}/",
    params={"count": 5},
    headers=HEADERS
).json()

print(f"\nNearby comparables ({comps['total_results']} found):")
for c in comps["comparables"][:3]:
    print(f"  {c['address']} — £{c['sold_let_price']:,} ({c['sold_let_date']})")

# Step 4: EPC deep-dive (construction age, floor area, certificate id)
epc = requests.get(
    f"{BASE}/api/epc-checker/{uprn}/",
    headers=HEADERS
).json()

print(f"\nEPC detail:")
print(f"  Score: {epc['current_energy_efficiency']} → potential {epc['potential_energy_efficiency']}")
print(f"  Floor area: {epc['epc_floor_area']}m²")
print(f"  Construction age: {epc.get('construction_age_band', 'N/A')}")
print(f"  Last assessed: {epc['last_epc_date']}")

The Python version is 41 lines including whitespace and comments. The JavaScript is 36 lines. Both go from a natural-language search query to a full property record with recent comparables — one resolved UPRN, three API calls.


Building the JavaScript widget

Server-side is one pattern. The more common use case is a live typeahead in a browser form. Here's a minimal implementation using vanilla JS and the Find endpoint — no framework, no dependencies:

<input id="address-input" type="text" placeholder="Start typing an address...">
<ul id="suggestions"></ul>

<script>
const API_KEY = "YOUR_API_KEY";
const input = document.getElementById("address-input");
const list = document.getElementById("suggestions");
let debounceTimer;

input.addEventListener("input", () => {
  clearTimeout(debounceTimer);
  const q = input.value.trim();
  if (q.length < 2) { list.innerHTML = ""; return; }

  debounceTimer = setTimeout(async () => {
    // Find: no API key needed
    const res = await fetch(`https://api.homedata.co.uk/api/address/find/?q=${encodeURIComponent(q)}`);
    const { suggestions } = await res.json();
    list.innerHTML = suggestions.map(s =>
      `<li data-uprn="${s.uprn}">${s.address}</li>`
    ).join("");
  }, 200);
});

list.addEventListener("click", async (e) => {
  const li = e.target.closest("li[data-uprn]");
  if (!li) return;

  const uprn = li.dataset.uprn;
  input.value = li.textContent;
  list.innerHTML = "";

  // Retrieve: API key required
  const res = await fetch(
    `https://api.homedata.co.uk/api/address/retrieve/${uprn}/?level=property`,
    { headers: { Authorization: `Api-Key ${API_KEY}` } }
  );
  const prop = await res.json();

  // Auto-fill your form fields
  document.getElementById("bedrooms").value = prop.bedrooms ?? "";
  document.getElementById("property-type").value = prop.property_type ?? "";
  document.getElementById("floor-area").value = prop.epc_floor_area ?? "";
  document.getElementById("epc-score").value = prop.current_energy_efficiency ?? "";
});
</script>

The 200ms debounce prevents hammering the Find endpoint on every keystroke. The Find call has no authentication — the browser can call it directly without exposing your key. The Retrieve call needs an API key, so in production you'd proxy it through your own backend rather than embedding the key in client-side JS.


Practical patterns

Pattern 1 — Mortgage or conveyancing intake form

The user types their address into a search field. You resolve it to a UPRN via Find, then call Retrieve at level=full to get predicted value, last sold price, construction age, and structural data. Then call the Flood Risk endpoint separately with the same UPRN for the NAFRA2 flood band. The form auto-fills. Your underwriters get pre-qualified data instead of applicant self-reporting.

Pattern 2 — Lettings platform listing creation

Agent types the property address. Find returns the UPRN. Retrieve at level=property populates: bedrooms, property type, floor area, EPC band. They can override manually if anything's wrong. Listing creation goes from a 5-minute form to a 30-second confirmation.

Pattern 3 — AVM input enrichment

Your automated valuation model needs clean structured inputs. Instead of asking users for property type and bedrooms (which they often get wrong), resolve the address to a UPRN and pull the data directly. Chain to Comparables for recent sales within 0.5 miles with matching bedrooms. You've built a lightweight AVM input pipeline in 50 lines.

Pattern 4 — Insurance quoting

Address search → Retrieve level=full → read current_energy_efficiency, construction_age_band, epc_floor_area. Separately call the Flood Risk endpoint with the same UPRN for NAFRA2 flood band. You have the key risk factors for a buildings insurance quote without the user filling in a single property characteristic.


API reference quick look

Endpoint Auth Returns
/api/address/find/?q= None (free) Address suggestions with UPRN
/api/address/postcode/{postcode}/ None (free) All addresses at a postcode
/api/address/retrieve/{uprn}/ Api-Key required Enriched property data (3 levels)
/api/comparables/{uprn}/ Api-Key required Comparable sales within 0.5 miles
/api/epc-checker/{uprn}/ Api-Key required EPC certificate (7 fields — score, floor area, construction age, EPC id)

Auth header format

All authenticated endpoints use Authorization: Api-Key YOUR_API_KEY — not Bearer. The Bearer format returns a 403.

Next steps

The free tier gives you 100 calls per month — enough to build and test a real integration. When you're ready to go live, the Starter plan at £49/month lifts that to 2,000 calls, and Growth (£149/month) gives you 10,000. Address find and postcode endpoints cost 2 calls each, and address retrieve costs 5 calls — reflecting real licensing costs but still far cheaper than per-lookup competitors.