When we started building TheWineTrip, we had a database of 45,000 wineries — but only about 4% had GPS coordinates. Without coordinates, we couldn't show them on a map, calculate driving routes, or answer “what wineries are near me?”. GPS coverage was a P0 blocker before we could ship.
Here's how we geocoded 3,500 wineries across five regions — Napa Valley, Bordeaux, Tuscany, Rioja, and Barossa Valley — and how you can replicate the approach for any location dataset.
The Problem: 45,000 Wineries, 4% GPS Coverage
Our winery data comes from a database that aggregates listings from across the web. The data is rich in names, addresses, and websites — but latitude and longitude are sparse. Rural wineries in Tuscany or the Barossa Valley often aren't mapped anywhere outside Google.
We needed to go from 4% to 90%+ coverage across 3,500 wineries for our five MVP regions. The constraint: do it cheaply, ideally for free.
Step 1: OpenStreetMap via Nominatim (Free, Low Hit Rate)
Our first attempt used Nominatim, the geocoding API built on top of OpenStreetMap. It's completely free, has no rate limit (with respectful usage), and covers most of the world.
We built a TypeScript script that queried Nominatim with each winery name and address:
const url = new URL("https://nominatim.openstreetmap.org/search");
url.searchParams.set("q", `${winery.name}, ${winery.city}, ${winery.country}`);
url.searchParams.set("format", "json");
url.searchParams.set("limit", "1");
const res = await fetch(url, {
headers: { "User-Agent": "TheWineTrip/1.0 (thewinetrip.com)" }
});
const data = await res.json();
if (data[0]) {
await updateWinery(winery.id, {
latitude: parseFloat(data[0].lat),
longitude: parseFloat(data[0].lon),
});
}Result: ~5% hit rate. Nominatim is excellent for cities and well-known landmarks, but most boutique wineries simply aren't in OpenStreetMap. A winery called “Domaine de la Romanée-Conti” is there. A small estate in Pomerol called “Château Feytit-Clinet” is not.
Step 2: Google Geocoding API (100% Hit Rate, ~$7.50)
Google's Geocoding API costs $5 per 1,000 requests. For 3,500 wineries, that's about $17.50 — but we only needed to geocode the wineries that Nominatim missed. With a 5% Nominatim hit rate, that left ~3,325 wineries.
We added Google as a fallback in our geocoding script:
async function geocodeWithGoogle(winery: Winery): Promise<Coords | null> {
const query = [winery.name, winery.city, winery.area, winery.country]
.filter(Boolean).join(", ");
const url = `https://maps.googleapis.com/maps/api/geocode/json?` +
`address=${encodeURIComponent(query)}&key=${GOOGLE_API_KEY}`;
const res = await fetch(url);
const data = await res.json();
if (data.status === "OK" && data.results[0]) {
const { lat, lng } = data.results[0].geometry.location;
return { latitude: lat, longitude: lng };
}
return null;
}Result: near 100% hit rate. Google's data coverage for wineries is extraordinary — even tiny estates in rural Tuscany or obscure Barossa properties appeared in their index.
We processed all 5 regions in parallel (rate-limited to 50 req/s), and the entire run took about 10 minutes. Final cost: approximately $7.50 in Google API credits.
Step 3: PostGIS for Geo Queries
With GPS coordinates in the database, we needed spatial queries — specifically, “find all wineries within 20km of this point.” We enabled the PostGIS extension on our Supabase (PostgreSQL) database and created a spatial index:
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE INDEX idx_places_geo
ON places USING GIST (
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)
)
WHERE latitude IS NOT NULL AND longitude IS NOT NULL;Then a PostgreSQL function to find nearby wineries:
CREATE OR REPLACE FUNCTION nearby_wineries(
p_lat DOUBLE PRECISION,
p_lng DOUBLE PRECISION,
p_radius_km DOUBLE PRECISION DEFAULT 15,
p_limit INT DEFAULT 10
)
RETURNS TABLE (...) AS $$
SELECT *, ST_Distance(
ST_SetSRID(ST_MakePoint(longitude::double precision, latitude::double precision), 4326)::geography,
ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography
) / 1000 AS distance_km
FROM places
WHERE category = 'winery'
AND latitude IS NOT NULL
AND ST_DWithin(
ST_SetSRID(ST_MakePoint(longitude::double precision, latitude::double precision), 4326)::geography,
ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography,
p_radius_km * 1000
)
ORDER BY distance_km
LIMIT p_limit;
$$ LANGUAGE sql STABLE;Results
| Region | Before | After | Coverage |
|---|---|---|---|
| Napa Valley | 9% | 100% | 164 wineries |
| Bordeaux | 6% | 97% | 1,247 wineries |
| Tuscany | 3% | 100% | 470 wineries |
| Rioja | 5% | 100% | 267 wineries |
| Barossa Valley | 8% | 100% | 666 wineries |
Total cost: approximately $7.50 in Google API credits. Time: ~10 minutes running parallel across all 5 regions. The result is a fully geocoded dataset powering our map view and “Nearby Wineries” feature.
Key Lessons
- 1.Don't try Nominatim alone for specialist data — it's excellent for general addresses but misses boutique businesses.
- 2.Google Geocoding is worth the cost for dense, specialist datasets. $7.50 for 100% coverage is a good deal.
- 3.Parallelise with rate limiting — we hit 50 requests/second across regions simultaneously.
- 4.Add a PostGIS spatial index before you run any geo queries — the performance difference is enormous.
- 5.Store coordinates as DOUBLE PRECISION in PostgreSQL — numeric(10,8) causes type cast errors in PostGIS functions.
Want to explore our winery data?
All 3,500 geocoded wineries are available through our public API. Free tier: 100 requests/day.