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 is one of the most common data tasks in UK property tech.
The underlying coordinate data comes from Ordnance Survey's Code-Point Open dataset, which maps postcode centroids. For UPRN-level precision, the April 2026 release of the OS Open UPRN dataset is the current quarterly refresh.
The Homedata API gives you two levels of geocoding precision:
- Postcode-level — centroid coordinates for any UK postcode (via
/api/postcode/{postcode}) - Property-level — exact UPRN coordinates for 29M+ individual properties (via
/api/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://neo.homedata.co.uk/api/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://neo.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://neo.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://neo.homedata.co.uk/api/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://neo.homedata.co.uk/api/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://neo.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 building with the free API →
100 calls/month · EPC, Land Registry, council tax, schools · No credit card
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.