Verifying vacation rental locations before they go live
Geocode, reverse-verify, and map-check every vacation rental listing before it publishes. REST patterns, failure modes, and a production pipeline.
A guest books a beachfront cottage. The listing says "oceanfront, 50 metres to the sand." The pin on the map sits in the middle of a roundabout 400 metres inland. They arrive at night, cannot find the property, and by morning the host has a one-star review and the platform has a chargeback dispute open.
This is not a rare edge case. Any platform that lets hosts type their own address and drag their own map pin will accumulate location errors. Some are innocent — the host typed the road name wrong, the geocoder snapped to the nearest street, nobody checked. Some are strategic — the listing claims to be "near the city centre" when it is a 40-minute drive. Both categories erode the trust that a travel platform survives on.
The solution is a verification pipeline that runs before the listing is published. Not a manual review queue — you cannot staff your way through ten thousand new listings a month. An automated three-step check: geocode the address the host provided, reverse-geocode the coordinates the host placed the pin at, and compare the two. Where they disagree beyond a tolerable threshold, hold the listing for human review. Where they agree, publish with confidence.
This post builds that pipeline end to end. You will get curl examples, working Python and Node code, a decision framework for thresholds, and a clear map of the failure modes that bite platforms who skip this step.
Why the problem is worse in travel than in other sectors
Property listings in real-estate and insurance sit relatively still. A house at 42 Maple Street is at 42 Maple Street when you list it and when you sell it. Vacation rentals have a different surface area of errors:
High host turnover and self-service onboarding. A professional estate agency submits structured data. A first-time Airbnb-style host types an address into a free-text box, often on a mobile phone, often from a foreign-language keyboard layout. Typos and partial addresses are the norm, not the exception.
Dense resort areas with ambiguous naming. A villa complex called "Sunset Views" in a Balearic resort town might have eight units with eight different postal sub-addresses, all sharing the same postcode. A geocoder will pick one of them. The host may have dragged the pin to a different one. The guest expects the third one.
Pins dragged for marketing, not accuracy. The temptation to drag a pin from "near the beach" to "on the beach" is real. A platform that does not verify pin position against the geocoded address will accumulate this kind of soft fraud until a journalist writes about it.
International address formats. A beach house in the Algarve, a gîte in the Dordogne, a trullo in Puglia — all have address conventions that differ from English-language expectations. A geocoder that handles 39 countries and 461M+ addresses gives you a meaningful coverage cushion; a pipeline that only handles US ZIP code logic will silently fail on 60% of your international inventory.
The three-step verification model
The model is simple enough to sketch in a sentence: forward-geocode the host's address, reverse-geocode the host's pin, compare results.
Step 1 — Forward geocode the host's address. Turn the text string into a canonical lat/lng with a confidence score. High confidence means the geocoder found a specific building-level match; low confidence means it fell back to a street centroid or postcode centroid.
Step 2 — Reverse geocode the host's pin. Turn the pin coordinate back into a structured address. This tells you what the geocoder thinks lives at the location the host chose on the map.
Step 3 — Compare and score. Compute the distance between the forward-geocoded coordinate and the host's pin. If the distance exceeds your threshold (more on choosing that threshold below), hold the listing. If the reverse-geocoded address does not match the forward-geocoded address at street level, hold the listing. If both checks pass, publish.
This is a pipeline you run once at listing-creation time and again whenever a host edits their address or moves their pin. It costs three API credits per check. At paid pricing starting from $54/month for 100,000 calls, that is under $0.002 per listing verification — a rounding error against any guest dispute cost.
Building the pipeline
Step 1: Forward-geocode the host's address
The geocoding call is the foundation. Everything downstream depends on getting a clean lat/lng with a reliable confidence score.
curl -G "https://csv2geo.com/api/v1/geocode" \
--data-urlencode "q=22 Harbour Road, St Ives, Cornwall, TR26 1PZ" \
--data-urlencode "api_key=$CSV2GEO_API_KEY"A well-formed UK address returns something like:
{
"results": [
{
"formatted": "22 Harbour Road, St Ives, Cornwall, TR26 1PZ, UK",
"lat": 50.2142,
"lng": -5.4793,
"confidence": 0.94,
"match_level": "building"
}
]
}Three fields matter for verification:
- `confidence` — a score from 0 to 1. Below 0.7, treat the result as untrustworthy and ask the host to correct the address before proceeding. A detailed breakdown of what this score means under the hood lives in Geocoding Confidence Scores Explained.
- `match_level` —
buildingis what you want.street,postcode, andlocalitymean the geocoder could not pin to a specific building and snapped to a centroid. A listing verified only to postcode level is not verified. - `lat`, `lng` — store these as the canonical coordinates, not the host's pin. The pin is what you are about to verify.
In Python:
import os
import requests
API = "https://csv2geo.com/api/v1"
KEY = os.environ["CSV2GEO_API_KEY"]
def geocode_address(address: str) -> dict:
r = requests.get(
f"{API}/geocode",
params={"q": address, "api_key": KEY},
timeout=15,
)
r.raise_for_status()
results = r.json().get("results", [])
if not results:
return {"error": "no_results"}
return results[0]In Node:
const API = 'https://csv2geo.com/api/v1';
const KEY = process.env.CSV2GEO_API_KEY;
async function geocodeAddress(address) {
const url = `${API}/geocode?q=${encodeURIComponent(address)}&api_key=${KEY}`;
const r = await fetch(url);
if (!r.ok) throw new Error(`geocode http ${r.status}`);
const body = await r.json();
if (!body.results?.length) return { error: 'no_results' };
return body.results[0];
}Step 2: Reverse-geocode the host's pin
The host has placed a pin on a map. You have a lat/lng from that interaction — maybe from your map widget's dragend event. Now ask the API: what address does this coordinate correspond to?
curl -G "https://csv2geo.com/api/v1/reverse" \
--data-urlencode "lat=50.2165" \
--data-urlencode "lng=-5.4801" \
--data-urlencode "api_key=$CSV2GEO_API_KEY"The response gives you a structured address for that pin location. Compare the street name, house number, and postcode against what came back from the forward geocode in Step 1.
def reverse_geocode_pin(lat: float, lng: float) -> dict:
r = requests.get(
f"{API}/reverse",
params={"lat": lat, "lng": lng, "api_key": KEY},
timeout=15,
)
r.raise_for_status()
results = r.json().get("results", [])
if not results:
return {"error": "no_results"}
return results[0]A deep treatment of what reverse-geocoding accuracy actually means in practice — and how distance in metres translates to human perception of "correct" — is in Reverse Geocoding Accuracy and the Distance Meters.
Step 3: Compute the displacement and compare addresses
Now you have two sets of coordinates: the forward-geocoded canonical position, and the host's pin. Compute the haversine distance between them.
import math
def haversine_m(lat1, lng1, lat2, lng2) -> float:
R = 6_371_000 # metres
phi1, phi2 = math.radians(lat1), math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlam = math.radians(lng2 - lng1)
a = math.sin(dphi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlam/2)**2
return 2 * R * math.asin(math.sqrt(a))
def verify_listing(address: str, pin_lat: float, pin_lng: float) -> dict:
geo = geocode_address(address)
if "error" in geo:
return {"status": "hold", "reason": "geocode_failed"}
if geo.get("confidence", 0) < 0.7:
return {"status": "hold", "reason": "low_confidence",
"confidence": geo["confidence"]}
if geo.get("match_level") not in ("building", "entrance"):
return {"status": "hold", "reason": "coarse_match",
"match_level": geo["match_level"]}
distance = haversine_m(geo["lat"], geo["lng"], pin_lat, pin_lng)
rev = reverse_geocode_pin(pin_lat, pin_lng)
return {
"status": "pass" if distance <= 150 else "hold",
"displacement_m": round(distance, 1),
"geocoded_address": geo.get("formatted"),
"pin_address": rev.get("formatted"),
"confidence": geo.get("confidence"),
"match_level": geo.get("match_level"),
}The 150-metre threshold is a starting point, not a law. The right value depends on your market:
- Dense urban listings (city-centre apartments, old-town townhouses): 50–80 m is a reasonable ceiling. City blocks are short and a 150 m error puts the guest in a different neighbourhood.
- Rural and coastal listings (farmhouses, beachfront cottages, mountain cabins): 200–300 m is more forgiving. Country lane addressing is imprecise and the host's pin on a wide-angle map will legitimately wobble by 100 m.
- Resort complexes with multiple units: The forward geocoder may match to the complex centroid rather than the individual unit entrance. In these cases, relax the distance threshold and tighten the address-text comparison instead.
Step 4: Handle the hold queue gracefully
A listing that fails verification should not silently disappear from the host's dashboard. The verification result needs to communicate the specific failure — "your address did not match your pin position by 340 metres" is actionable; "your listing is under review" is not.
A lightweight Node handler that translates verification results into host-facing messages:
const HOLD_MESSAGES = {
geocode_failed: 'We could not locate this address. Please check for typos or add a postcode.',
low_confidence: 'We found a partial match for this address but could not confirm the building. Please check the street number and postcode.',
coarse_match: 'Your address matched a postcode area rather than a specific building. Please provide the full street address.',
displacement: (m) =>
`Your map pin is ${m} metres from where we geocoded your address. ` +
`Please drag the pin to the correct location or correct the address.`,
};
function listingHoldMessage(verificationResult) {
const { status, reason, displacement_m } = verificationResult;
if (status === 'pass') return null;
if (reason === 'displacement') return HOLD_MESSAGES.displacement(displacement_m);
return HOLD_MESSAGES[reason] ?? 'Location verification failed. Please review your address and pin.';
}The host gets a specific, correctable message. The platform gets a structured reason code it can log, aggregate, and report on. A weekly report of "top hold reasons" is a useful signal for improving the onboarding UX — if 30% of your holds are low_confidence on Italian addresses, you know to add a postcode hint to the onboarding form for Italian properties.
Step 5: Persist the verification state and re-run on edit
Verification is not a one-shot event. A host who passes verification on Monday and then edits their address on Wednesday has triggered a new verification. Your data model needs a location_verified_at timestamp and a location_verification_status field. Any edit to address or pin_lat/pin_lng resets the status to pending and enqueues a new check.
import datetime
def on_listing_address_updated(listing_id: str, new_address: str,
pin_lat: float, pin_lng: float, db):
# Immediately unpublish pending re-verification
db.execute(
"UPDATE listings SET location_status='pending' WHERE id=?",
(listing_id,)
)
result = verify_listing(new_address, pin_lat, pin_lng)
status = "verified" if result["status"] == "pass" else "hold"
db.execute(
"""UPDATE listings
SET location_status=?,
location_verified_at=?,
location_displacement_m=?,
location_match_level=?
WHERE id=?""",
(status, datetime.datetime.utcnow().isoformat(),
result.get("displacement_m"),
result.get("match_level"),
listing_id)
)
return resultThe geocoding result itself should be cached so that a subsequent re-run for the same address does not spend another credit needlessly. The address-to-coordinate mapping is stable — addresses do not move. See Caching Geocoding Results — 90% Cost Reduction for the exact caching layer pattern. The short version: key on a normalised address string, TTL of 90 days, and you will absorb repeated verifications of the same address at near-zero cost.
Failure modes to design around
The three-step pipeline above handles the common cases. Here are the failure modes that catch teams out in production.
The geocoder returns a confident wrong result. A confidence score of 0.92 does not mean the coordinate is at the building entrance — it means the geocoder is confident it found the right building centroid. For dense apartment complexes, the centroid might be 60 m from unit 1A and 180 m from unit 12F. If your verification threshold is 150 m, unit 12F hosts will consistently fail without having done anything wrong. The fix is to loosen the threshold for addresses that contain a unit number and strengthen the address-text comparison instead.
The host's pin is in the sea. For coastal properties, the address is on land and the host has heroically dragged the pin onto the ocean "for the view." The displacement from the geocoded land address to the marine pin can be 200+ metres even though the property is genuinely oceanfront. Detect this by checking whether the reverse-geocode of the pin returns a marine or null result — if it does, flag the pin as invalid_pin_location rather than treating it as a displacement failure.
International addresses with no building-level coverage. The geocoder covers 39 countries, but building-level match quality varies. In some rural markets, even a correct address resolves only to street or postcode level. If you hold every non-building-level match in these markets, you will hold most of your inventory. The better approach: apply the coarse_match hold only in markets where building-level coverage is reliable (Western Europe, US, Canada, Australia), and fall back to a human-review queue for markets where it is not.
Rate limits during bulk onboarding campaigns. If a marketing campaign brings in 5,000 new hosts over a weekend, your verification queue will spike. Design the queue processor with exponential backoff and concurrency caps. The concurrency tuning guidance in Concurrency Tuning for Geocoding at Scale applies directly — the same principles that govern bulk address enrichment govern a spiky verification queue.
Idempotency when the queue retries. A verification job that fails halfway through and is retried must not double-charge or produce inconsistent state. Make the verification function idempotent: check whether a fresh verification result already exists in the database before calling the API. The idempotency pattern is covered in detail in Idempotent Geocoding — Safe to Retry.
What verification does not catch
Be honest with your product team about the scope. This pipeline catches positional errors — the address does not match the pin. It does not catch:
Fictitious properties. A host can enter a real address that belongs to a building they do not control. The coordinates will check out perfectly. Fictitious property fraud needs a different defence — phone verification, document upload, or payment-to-real-address checks.
Accurate coordinates, misleading description. "Beachfront" is a narrative claim, not a coordinate. The pin can be perfectly verified at 47.2134, -1.5432, and the listing can still lie about what you see from the window. Verification is a floor on location accuracy, not a ceiling on marketing honesty.
Elevation and terrain context. A listing that claims to be "mountain retreat" when it sits at 40 m in a suburban valley is not something coordinate verification catches. If terrain context matters for your platform — ski chalets, mountain hikes, coastal flood exposure — add an elevation check using the /api/v1/elevation endpoint. The anchor probes give you a sanity-check baseline: Denver returns 1,597 m, Miami returns 1 m, Tokyo returns 40 m. A listing that claims to be a ski chalet but geocodes to 40 m elevation deserves a flag. See Enriching Property Data with Elevation for the full elevation pipeline.
Cost model for a real platform
A platform onboarding 10,000 new listings per month, with an average of 1.4 verification attempts per listing (some hosts need one correction cycle):
- Forward geocode: 14,000 calls/month
- Reverse geocode: 14,000 calls/month
- Total: 28,000 calls/month
At the entry paid tier (100,000 calls for $54/month), verification for the entire onboarding flow sits comfortably inside one billing bracket alongside all your other geocoding work. The free tier covers 3,000 calls/day — enough for a pilot covering 45,000 verifications per month, which is sufficient to validate the pipeline at realistic scale before committing to a paid plan.
The caching layer for repeat verifications of unchanged addresses cuts effective credit consumption further. A host who edits their listing title but not their address should not trigger a new geocoding call. Cache the geocoded coordinate against the normalised address string, and the marginal cost of a re-verification drops to a single reverse geocode.
Frequently Asked Questions
What confidence score threshold should I use to hold a listing?
0.7 is a reasonable starting point. Below 0.7 the geocoder has typically fallen back to a street or postcode centroid, and the coordinate is unreliable for pin-comparison purposes. Tune it by market: dense urban markets with good building-level coverage can push to 0.8; rural markets in countries with less complete address data should stay at 0.65 or lower. The explanation of what the score actually encodes is in Geocoding Confidence Scores Explained.
What distance threshold should I use to flag a pin mismatch?
150 m is a sensible default for urban listings. For rural and coastal properties with imprecise local addressing, 250–300 m is more forgiving without being meaningless. Segment by listing type — your platform likely has a property_type field already — and apply different thresholds per segment rather than one global value.
Does this pipeline work for listings outside the 39 supported countries?
No, and you should not pretend it does. Properties outside the covered countries will return low-confidence or no-result geocoding responses. Route those to a manual review queue and surface a clear "we cannot auto-verify this address, please upload a photo of the property entrance" CTA to the host.
Should I verify on every listing update or only on address changes?
Verify on address text change and on pin coordinate change. Do not re-verify on title, description, or photo updates — those do not affect location. Track last_address_hash and last_pin_hash in your database and only enqueue a re-verification when either changes. This keeps your credit consumption predictable.
What happens if the API is unavailable when a host submits their listing?
Do not block the host's submission. Accept the listing, set location_status = pending, and queue the verification job with retry logic. The host should see a "your location is being verified" message rather than an error. Use exponential backoff on the retry queue so a transient API outage does not hammer the endpoint when it recovers. The retry pattern is covered in Exponential Backoff — When to Retry, When to Stop.
Can I use this pipeline to retroactively audit existing listings?
Yes, and you should. A batch job that runs the three-step verification across your existing inventory will surface listings that passed manual review but have drifted (hosts who edited coordinates after going live) and listings that predate your verification system. Batch at 500 geocoding calls per minute, cache aggressively, and expect a 2–5% hold rate on a mature inventory that was previously unverified. That 2–5% represents real guest-experience risk you are actively retiring.
Related Articles
- Reverse Geocoding Accuracy and the Distance in Metres
- Geocoding Confidence Scores Explained
- Caching Geocoding Results — 90% Cost Reduction
- Idempotent Geocoding — Safe to Retry
- Enriching Property Data with Elevation
---
*I.A. / CSV2GEO Creator*
Use our batch geocoding tool to convert thousands of addresses to coordinates in minutes. Start with 100 free addresses.
Try Batch Geocoding Free →