UK Postcode Geocoding API: Convert Postcodes to Coordinates (Python & JS)
Convert any UK postcode to latitude/longitude coordinates, plus UPRN-level precision for individual properties. Includes batch geocoding, proximity search, and distance calculations with Python and JavaScript examples.
The problem: postcodes aren't coordinates
Every UK property has a postcode. But if you're building a mapping feature, calculating distances, or running geospatial queries, you need latitude and longitude — not a string like "SW1A 1AA". Converting postcodes to coordinates (geocoding) is one of the most common data tasks in UK property tech.
The Homedata API gives you two levels of geocoding precision:
- Postcode-level — centroid coordinates for any UK postcode (via
/api/v1/postcode/{postcode}) - Property-level — exact UPRN coordinates for 29M+ individual properties (via
/api/v1/address/retrieve/{uprn})
Quick start: postcode to coordinates
The postcode profile endpoint returns the centroid latitude/longitude along with demographics, house prices, and area statistics. Here's a minimal geocoding call:
curl "https://homedata.co.uk/api/v1/postcode/SW1A1AA" \
-H "X-API-Key: hd_live_your_key_here"
{
"postcode": "SW1A 1AA",
"latitude": 51.50100,
"longitude": -0.14157,
"district": "Westminster",
"county": "Greater London",
"country": "England",
"region": "London",
"parliamentary_constituency": "Cities of London and Westminster",
"average_price": 1285000,
"median_price": 975000,
"transaction_count": 47,
...
}
Python: batch geocode postcodes
Here's a complete Python script that geocodes a list of postcodes and writes the results to a CSV file. Handles rate limiting and missing postcodes gracefully.
import requests
import csv
import time
API_KEY = "hd_live_your_key_here"
BASE_URL = "https://homedata.co.uk/api/v1"
def geocode_postcode(postcode: str) -> dict | None:
"""
Convert a UK postcode to lat/lng coordinates.
Returns dict with postcode, lat, lng or None if not found.
"""
# Normalise: strip spaces for the URL path
clean = postcode.replace(" ", "").upper()
resp = requests.get(
f"{BASE_URL}/postcode/{clean}",
headers={"X-API-Key": API_KEY},
timeout=10,
)
if resp.status_code == 404:
return None # invalid or terminated postcode
resp.raise_for_status()
data = resp.json()
return {
"postcode": data.get("postcode", postcode),
"latitude": data.get("latitude"),
"longitude": data.get("longitude"),
"district": data.get("district"),
"region": data.get("region"),
}
def batch_geocode(postcodes: list[str], output_csv: str):
"""Geocode a list of postcodes and write results to CSV."""
results = []
failed = []
for i, pc in enumerate(postcodes, 1):
print(f" [{i}/{len(postcodes)}] Geocoding {pc}...", end=" ")
try:
result = geocode_postcode(pc)
if result:
results.append(result)
print(f"→ ({result['latitude']}, {result['longitude']})")
else:
failed.append(pc)
print("→ not found")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
print("→ rate limited, waiting 2s...")
time.sleep(2)
# Retry once
try:
result = geocode_postcode(pc)
if result:
results.append(result)
except Exception:
failed.append(pc)
else:
failed.append(pc)
print(f"→ error: {e}")
# Respect rate limits: ~2 requests per second on free tier
time.sleep(0.5)
# Write CSV
if results:
with open(output_csv, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=results[0].keys())
writer.writeheader()
writer.writerows(results)
print(f"\nDone: {len(results)} geocoded, {len(failed)} failed → {output_csv}")
if failed:
print(f"Failed postcodes: {', '.join(failed)}")
return results
# Example: geocode office locations
postcodes = [
"EC2R 8AH", # Bank of England
"SW1A 1AA", # Buckingham Palace
"M1 1AE", # Manchester city centre
"LS1 1UR", # Leeds
"B1 1BB", # Birmingham
"EH1 1YZ", # Edinburgh (if data available)
"CF10 1EP", # Cardiff
"BS1 5TR", # Bristol
"NE1 7RU", # Newcastle
"NG1 5FW", # Nottingham
]
batch_geocode(postcodes, "office_locations.csv")
JavaScript / Node.js: geocode in your API
For server-side JavaScript applications — a common pattern when building Express/Fastify backends that need to enrich incoming data with coordinates.
const API_KEY = 'hd_live_your_key_here';
const BASE_URL = 'https://homedata.co.uk/api/v1';
interface GeocodedPostcode {
postcode: string;
latitude: number;
longitude: number;
district?: string;
region?: string;
}
async function geocodePostcode(postcode: string): Promise<GeocodedPostcode | null> {
const clean = postcode.replace(/\s/g, '').toUpperCase();
const response = await fetch(`${BASE_URL}/postcode/${clean}`, {
headers: { 'X-API-Key': API_KEY },
});
if (response.status === 404) return null;
if (!response.ok) throw new Error(`API error: ${response.status}`);
const data = await response.json();
return {
postcode: data.postcode,
latitude: data.latitude,
longitude: data.longitude,
district: data.district,
region: data.region,
};
}
/**
* Calculate straight-line distance between two points (Haversine formula).
* Returns distance in kilometres.
*/
function haversineKm(
lat1: number, lng1: number,
lat2: number, lng2: number
): number {
const R = 6371; // Earth's radius in km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(lat1 * Math.PI / 180) *
Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// Example: find distance between two postcodes
async function distanceBetween(pc1: string, pc2: string): Promise<string> {
const [a, b] = await Promise.all([
geocodePostcode(pc1),
geocodePostcode(pc2),
]);
if (!a || !b) return 'Could not geocode one or both postcodes';
const km = haversineKm(a.latitude, a.longitude, b.latitude, b.longitude);
return `${a.postcode} → ${b.postcode}: ${km.toFixed(1)} km`;
}
// Run
distanceBetween('SW1A 1AA', 'M1 1AE').then(console.log);
// → SW1A 1AA → M1 1AE: 262.4 km
UPRN-level precision: exact property coordinates
For property-level applications — valuation, surveying, delivery routing — postcode centroids aren't precise enough. A single postcode can cover 15–100 properties spread across several streets. The Homedata address retrieve endpoint returns exact UPRN coordinates:
curl "https://homedata.co.uk/api/v1/address/find/SW1A1AA" \
-H "X-API-Key: hd_live_your_key_here"
# Returns:
# [
# { "uprn": "100023336956", "address": "10 DOWNING STREET, LONDON, SW1A 1AA" },
# { "uprn": "100023336957", "address": "11 DOWNING STREET, LONDON, SW1A 1AA" },
# ...
# ]
curl "https://homedata.co.uk/api/v1/address/retrieve/100023336956" \
-H "X-API-Key: hd_live_your_key_here"
# Returns property details including exact coordinates:
# {
# "uprn": "100023336956",
# "address_line_1": "10 Downing Street",
# "postcode": "SW1A 2AA",
# "latitude": 51.50344,
# "longitude": -0.12758,
# "epc_rating": "D",
# "property_type": "Terraced",
# ...
# }
Use case: proximity search (find properties within X km)
A common pattern: given a centre point (e.g. an office, a school, a train station), find all properties within a radius. Here's how to combine geocoding with the listings endpoint:
import requests
import math
API_KEY = "hd_live_your_key_here"
BASE = "https://homedata.co.uk/api/v1"
def haversine_km(lat1, lng1, lat2, lng2):
"""Calculate distance between two points in kilometres."""
R = 6371
dlat = math.radians(lat2 - lat1)
dlng = math.radians(lng2 - lng1)
a = (math.sin(dlat / 2) ** 2 +
math.cos(math.radians(lat1)) *
math.cos(math.radians(lat2)) *
math.sin(dlng / 2) ** 2)
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
def find_properties_near(postcode: str, radius_km: float = 1.0):
"""
Find properties for sale within radius_km of a postcode centroid.
1. Geocode the postcode
2. Search listings in the area
3. Filter by exact distance
"""
# Step 1: Geocode the centre point
geo = requests.get(
f"{BASE}/postcode/{postcode.replace(' ', '')}",
headers={"X-API-Key": API_KEY},
timeout=10,
).json()
centre_lat = geo["latitude"]
centre_lng = geo["longitude"]
print(f"Centre: {geo['postcode']} ({centre_lat}, {centre_lng})")
# Step 2: Search listings in the postcode area
# Use the outcode (first half) as a broad filter
outcode = postcode.split()[0]
listings = requests.get(
f"{BASE}/listings/search",
params={
"postcode": outcode,
"transaction_type": "sale",
"limit": 100,
},
headers={"X-API-Key": API_KEY},
timeout=15,
).json().get("listings", [])
# Step 3: Filter by exact distance
nearby = []
for listing in listings:
lat = listing.get("lat") or listing.get("latitude")
lng = listing.get("lng") or listing.get("longitude")
if lat and lng:
dist = haversine_km(centre_lat, centre_lng, lat, lng)
if dist <= radius_km:
listing["distance_km"] = round(dist, 2)
nearby.append(listing)
nearby.sort(key=lambda x: x["distance_km"])
print(f"Found {len(nearby)} properties within {radius_km}km:")
for p in nearby[:5]:
print(f" {p.get('address', 'N/A')} — {p['distance_km']}km — £{p.get('price', 0):,}")
return nearby
# Example: properties within 0.5km of Kings Cross
results = find_properties_near("N1C 4QP", radius_km=0.5)
Comparison: postcode vs UPRN geocoding
| Feature | Postcode centroid | UPRN property-level |
|---|---|---|
| Accuracy | ~100m (postcode centre) | ~1m (building entrance) |
| Coverage | 1.8M UK postcodes | 29M+ properties |
| API calls | 1 call (weight: 2) | 2 calls (find + retrieve, weight: 2 + 5) |
| Best for | Area analysis, heatmaps, catchment areas | Property mapping, valuation, surveying |
| Extra data | Demographics, prices, schools | EPC, property type, floor area, build year |
API call costs
Geocoding calls use weighted API credits. On the Free tier (100 calls/month), you can geocode up to 50 postcodes or ~14 individual properties per month — enough to test and prototype. On Growth (10,000 calls), that's 5,000 postcodes or ~1,400 UPRN lookups.
GET /postcode/{postcode}— weight 2 (1 call = 2 credits)GET /address/find/{postcode}— weight 2 (returns all addresses at a postcode)GET /address/retrieve/{uprn}— weight 5 (full property details + coordinates)
Frequently asked questions
What is UK postcode geocoding?
Postcode geocoding converts a UK postcode (e.g. SW1A 1AA) into a latitude/longitude coordinate pair. This enables mapping, distance calculations, and geospatial analysis for any UK address. Homedata's postcode endpoint also returns area-level statistics (average prices, demographics, schools) alongside the coordinates.
How accurate is postcode-level geocoding?
Postcode centroids are typically accurate to within 100 metres of the actual address. For higher precision, use UPRN-level geocoding which returns coordinates for the specific property (accurate to approximately 1 metre).
What's the difference between postcode and UPRN geocoding?
Postcode geocoding returns the centroid (centre point) of a postcode area, which may contain 15–100 addresses. UPRN geocoding returns the exact coordinates of a specific property. Use postcode for area-level analysis and heatmaps; use UPRN for property-level precision (surveying, valuations, delivery routing).
Is there a free UK geocoding API?
Yes — Homedata offers a free tier with 100 API calls per month. That's enough for ~50 postcode geocoding requests (each costs 2 credits). No credit card required. Sign up and get your API key in 30 seconds.
Start geocoding UK postcodes
Get a free API key and geocode your first postcode in under a minute. 100 calls/month, no credit card required.