Flagging suspicious addresses in order screening with geocoding signals
Use geocoding confidence scores and IP-vs-shipping distance as fraud review signals at checkout. Patterns, code, and failure modes for engineers.
Most order-screening pipelines put card signals at the centre — BIN country, velocity, card-not-present risk, issuing bank rules. The shipping address is treated as a deliverability problem: does it exist, will the carrier accept it? That is the wrong frame. The shipping address is also a signal surface. A geocoding API that can forward-geocode an address and return a confidence score at checkout, combined with an IP geolocation call that tells you roughly where the buyer's device is, gives you two cheap signals that are difficult to fake cleanly and straightforward to reason about in a rules engine.
This post covers those signals precisely: what they are, what they are not, where they fail, and how to wire them into a checkout pipeline without blocking on a geocoding round-trip. No invented fraud-loss statistics, no "auto-decline this class of address" oversimplification. These are review signals — you weigh them in your rules alongside the card signals you already have.
One clarification before we begin: everything here is distinct from address *validation* at checkout, which is about deliverability and typo correction. That post exists — Validating Shipping Addresses at Checkout — and if you have not shipped that first, do it first. This post layers risk signals on top of the same geocoding calls, not instead of them.
The three signals and what they mean
Before any code, let us be precise about what each signal is measuring and what it cannot measure.
Signal 1: Geocoding confidence. When you forward-geocode a shipping address, the API returns a confidence score alongside the coordinates. That score reflects how cleanly the address string resolved against the address corpus — did the address match unambiguously at the rooftop level, or did the engine have to fall back to street centreline, or to the postal code centroid? A rooftop-level match at high confidence means the address is real and specific. A postcode-centroid match at low confidence means the address string did not resolve cleanly — either it does not exist in the address data, or it is malformed, or it is a valid-looking but non-existent fabrication.
What it cannot measure: a fraudster who knows a real, high-confidence address in the city where they want packages delivered will produce a high-confidence score. Confidence tells you the address is real and parseable; it does not tell you who lives there.
Signal 2: IP-to-shipping distance. An IP geolocation call on the buyer's checkout IP returns an approximate city-level location. The distance between that location and the shipping address is a signal. A buyer in Edinburgh shipping to their Edinburgh home address is unremarkable. A buyer whose IP resolves to a residential ISP in one country shipping to a freight-forwarder hub in another is a different situation. The distance itself is not the signal — distance is normal for gift orders, for corporate purchasing, for shipping to a holiday address — but an extreme distance combined with a low-confidence address score and a freight-forwarder-dense postcode is a reasonable REVIEW flag.
What it cannot measure: anyone using a VPN, a mobile data connection, a corporate proxy, or who is simply travelling will produce a misleading IP location. IP geolocation is an assistive signal, not a ground truth. Treat it as one input among several.
Signal 3: Freight-forwarder concentration. This one you maintain, not the API. Certain postal codes are dominated by parcel-forwarding and reshipping operations. If your order history shows that n% of chargebacks came from a small set of postcodes, those postcodes go on a watch list that you check at checkout. The geocoding call gives you the standardised postcode for any messy address input; your rules engine checks it against your internal list. CSV2GEO does not maintain a freight-forwarder deny list, and neither does any geocoding API — this is your data, built from your order history.
What CSV2GEO does and does not provide
Be clear on the scope. CSV2GEO provides:
- Forward geocoding with confidence scores — the signal in item 1 above.
- IP geolocation — the location input for item 2 above. The integration pattern for adding IP geolocation alongside geocoding is covered in detail in Add IP Geolocation to Your Geocoding Stack.
- A batch geocoding tool (the WEB batch tool) that accepts address rows and returns geocoded results, billed per address row — useful for rescoring a historical order book.
CSV2GEO does not provide: a fraud score, a deny list, a fraud endpoint, or an auto-decline recommendation. The platform gives you geocoding signals. What you do with those signals — thresholds, weights, rule combinations, manual review queues — is your rules engine, your fraud team's call, and your business context.
This is the correct design. An order that looks suspicious on geocoding signals alone might be a perfectly legitimate gift from a buyer who is travelling internationally and shipping to a rural address that happens to resolve at postcode level. Auto-declining on a geocoding signal without your rules context would create customer experience problems that cost more than any fraud saving. Use these as REVIEW signals.
Wiring confidence scores into the checkout path
The forward geocoding call happens at the point where the buyer submits the shipping address — ideally on the address form itself, as a background call triggered on blur of the postcode field, so the result is ready before the buy button is pressed.
Step 1: Geocode the submitted address
curl -s "https://csv2geo.com/api/v1/geocode" \
--data-urlencode "q=742 Evergreen Terrace, Springfield, IL 62701" \
--data-urlencode "api_key=$CSV2GEO_KEY" \
-G | jq '{lat, lng, confidence, match_type, postcode}'A clean rooftop match returns something like:
{
"lat": 39.7837,
"lng": -89.6501,
"confidence": 0.97,
"match_type": "rooftop",
"postcode": "62701"
}A degraded match — street centreline or postcode centroid — returns a lower confidence and a less specific match_type. That degradation is the signal you are looking for.
In Python:
import os
import requests
API = "https://csv2geo.com/api/v1/geocode"
KEY = os.environ["CSV2GEO_KEY"]
def geocode_shipping_address(address_string):
r = requests.get(
API,
params={"q": address_string, "api_key": KEY},
timeout=5,
)
r.raise_for_status()
result = r.json().get("results", [{}])[0]
return {
"lat": result.get("lat"),
"lng": result.get("lng"),
"confidence": result.get("confidence"),
"match_type": result.get("match_type"),
"postcode": result.get("postcode"),
}In Node:
const API = 'https://csv2geo.com/api/v1/geocode';
const KEY = process.env.CSV2GEO_KEY;
async function geocodeShippingAddress(addressString) {
const url = `${API}?q=${encodeURIComponent(addressString)}&api_key=${KEY}`;
const r = await fetch(url, { signal: AbortSignal.timeout(5000) });
if (!r.ok) throw new Error(`geocode failed: ${r.status}`);
const data = await r.json();
const result = data.results?.[0] ?? {};
return {
lat: result.lat,
lng: result.lng,
confidence: result.confidence,
matchType: result.match_type,
postcode: result.postcode,
};
}The timeout is intentionally tight — 5 seconds — because this call is on the checkout critical path. If the geocoding call times out, log it and continue; do not block the order. A timed-out geocoding call is not a fraud signal; it is a network problem. Retry asynchronously after the order is placed and score the address offline.
Step 2: Fetch the IP geolocation in parallel
Do not sequence the geocoding and IP geolocation calls — fire them in parallel. The buyer's checkout IP is available the moment the form submits; there is no reason to wait for the geocoding result before starting the IP lookup.
import asyncio
import os
import httpx # async-friendly; swap for requests + ThreadPoolExecutor if needed
GEO_API = "https://csv2geo.com/api/v1/geocode"
IP_API = "https://csv2geo.com/api/v1/ip"
KEY = os.environ["CSV2GEO_KEY"]
async def checkout_signals(address_string, buyer_ip):
async with httpx.AsyncClient(timeout=5.0) as client:
geo_task = client.get(GEO_API, params={"q": address_string, "api_key": KEY})
ip_task = client.get(IP_API, params={"ip": buyer_ip, "api_key": KEY})
geo_r, ip_r = await asyncio.gather(geo_task, ip_task, return_exceptions=True)
geo = {}
if not isinstance(geo_r, Exception) and geo_r.status_code == 200:
geo = geo_r.json().get("results", [{}])[0]
ip_loc = {}
if not isinstance(ip_r, Exception) and ip_r.status_code == 200:
ip_loc = ip_r.json()
return geo, ip_locBoth calls run in parallel; the total checkout latency impact is the slower of the two, not the sum. In practice, on a warm API connection with a local timeout budget of 5 seconds, both usually complete well inside the budget.
In Node, the pattern is identical with Promise.all:
async function checkoutSignals(addressString, buyerIp) {
const geoUrl = `https://csv2geo.com/api/v1/geocode` +
`?q=${encodeURIComponent(addressString)}&api_key=${KEY}`;
const ipUrl = `https://csv2geo.com/api/v1/ip` +
`?ip=${encodeURIComponent(buyerIp)}&api_key=${KEY}`;
const [geoR, ipR] = await Promise.allSettled([
fetch(geoUrl, { signal: AbortSignal.timeout(5000) }),
fetch(ipUrl, { signal: AbortSignal.timeout(5000) }),
]);
const geo = geoR.status === 'fulfilled' && geoR.value.ok
? (await geoR.value.json()).results?.[0] ?? {}
: {};
const ipLoc = ipR.status === 'fulfilled' && ipR.value.ok
? await ipR.value.json()
: {};
return { geo, ipLoc };
}Promise.allSettled is the right primitive here — you want both results (or their failure modes) regardless of whether one rejects. Promise.all would short-circuit on the first failure.
Step 3: Compute the IP-to-shipping distance
Once you have both results, computing the great-circle distance is a dozen lines of maths. No third-party library needed.
import math
def haversine_km(lat1, lng1, lat2, lng2):
R = 6371.0
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 R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
def ip_to_shipping_distance(geo, ip_loc):
ship_lat = geo.get("lat")
ship_lng = geo.get("lng")
ip_lat = ip_loc.get("lat")
ip_lng = ip_loc.get("lng")
if None in (ship_lat, ship_lng, ip_lat, ip_lng):
return None # missing data — not a signal
return haversine_km(ip_lat, ip_lng, ship_lat, ship_lng)The function returns None when either location is unavailable — a missing result is not a fraud signal, it is an absence of evidence. Treat it as neutral in your rules engine.
Step 4: Assemble the signal bundle and hand it to your rules engine
The output of the geocoding layer is a plain dictionary or object that your existing fraud rules engine consumes. It should not make a decision; it should report facts.
def build_geocoding_signals(address_string, buyer_ip):
geo, ip_loc = asyncio.run(checkout_signals(address_string, buyer_ip))
distance_km = ip_to_shipping_distance(geo, ip_loc)
return {
"geocode_confidence": geo.get("confidence"), # 0.0–1.0
"geocode_match_type": geo.get("match_type"), # rooftop/street/postcode
"shipping_postcode": geo.get("postcode"), # standardised
"ip_country": ip_loc.get("country_code"),
"ip_to_shipping_km": distance_km, # float or None
"geocode_available": bool(geo.get("lat")),
"ip_location_available": bool(ip_loc.get("lat")),
}Your rules engine then evaluates something like:
geocode_confidence < 0.5→ flag for address reviewgeocode_match_type == "postcode"→ flag for address review (resolved only to postcode centroid)ip_to_shipping_km > 2000 AND geocode_confidence < 0.7→ flag for combined reviewshipping_postcode in freight_forwarder_watch_list→ flag for freight-forwarder review- Any two flags → route to manual review queue, not auto-decline
The thresholds above are illustrative starting points, not recommended values. Set your thresholds against your own historical order data. A 0.5 confidence threshold might catch noise you care about or might create a false-positive rate that swamps your review team — calibrate on your data.
Step 5: Rescore your historical order book with the WEB batch tool
Before setting live thresholds, you want to know what your existing order history looks like against these signals. The WEB batch tool accepts a CSV of address rows and returns geocoded results, billed per address row. Upload a year of orders — addresses only, no card data, no PII you do not need in a geocoding pipeline — and you have a scored corpus to calibrate against.
Practically:
- Export
order_id, shipping_address_full, known_outcome(whereknown_outcomeisclean,chargeback, ormanual_review) from your order management system. - Upload to the WEB batch tool at csv2geo.com. Each address row costs one credit. Paid tier starts at $54/month for 100,000 calls; pricing is live at csv2geo.com/pricing/api. The free tier (3,000 calls/day) handles small samples.
- Download the enriched CSV with confidence scores and standardised postcodes added.
- Join back to
known_outcomeand examine the distribution: do chargebacks skew toward lower confidence scores? Does your freight-forwarder watch list postcode set show up disproportionately in the chargeback cohort? The answer to those questions is the empirical basis for your threshold calibration.
This is the correct order of operations: calibrate offline first, then set live thresholds. Setting thresholds in production on intuition and adjusting under fire is a painful way to tune a fraud system.
Failure modes to plan for
Three failure modes that will bite you if you do not handle them explicitly.
Geocoding timeouts on the checkout critical path. The call is fast, but timeouts happen. As noted above: if the geocoding call does not return within your timeout budget, record that the signal is unavailable, let the order through, and score the address asynchronously after placing. A geocoding timeout is not a fraud signal — it is an infrastructure event. Do not let it become a false-positive or a dropped order.
VPNs and corporate proxies contaminate IP distance. A buyer using a business VPN for security reasons will produce an IP location in a data centre somewhere unrelated to their physical location. The IP-to-shipping distance will be enormous. This signal fires disproportionately on security-conscious buyers, remote workers, and anyone in a country where VPN use is culturally normal. If your IP distance rule is catching a large fraction of orders and most of them turn out to be clean, the VPN contamination rate is probably the explanation. Add an "IP is known datacenter/proxy range" flag — the IP geolocation response will often include this — and suppress the distance signal when it fires.
Rural and newly-built addresses resolve at lower confidence legitimately. A freshly built house on a rural road in a county where address data lags construction by 12-18 months will geocode at postcode level, not rooftop level. Low confidence is not a fraud signal in isolation — it is an artefact of address-data coverage. If you operate in markets with significant rural or new-construction order volume, the confidence threshold that makes sense for a dense urban market will produce unacceptable false-positive rates in those markets. Consider market-segmented thresholds rather than a global value.
Using these signals without harming legitimate customers
The responsible deployment of these signals requires being clear about what they can and cannot establish.
They are probabilistic, not deterministic. A low-confidence address in combination with a large IP distance does not mean the buyer is fraudulent. It means the order shares features with orders that have been associated with fraud. The appropriate response is REVIEW, not DECLINE.
The review queue needs a clear resolution path. If you flag an order for manual review, the reviewer needs enough information to resolve it — typically a "contact the buyer to verify the delivery address" workflow. Do not build a flag that dumps orders into a queue with no resolution path; that creates the same customer experience problem as an auto-decline, just with a longer lag.
Do not log the buyer's IP in a geocoded-address record without considering your privacy obligations. The combination of IP address and physical shipping address is PII in most jurisdictions. Ensure your data-retention policy covers this combination and that the geocoding pipeline does not write it to log stores that have broader retention than your order management system.
FAQ
Does CSV2GEO provide a fraud score or decline recommendation? No. The platform provides geocoding confidence scores and IP geolocation — raw signals. The fraud score, the rules that combine signals, and any decline or review decision are entirely yours. This is intentional: fraud rules are specific to your merchant category, your geography, your customer base, and your chargeback tolerance. A one-size-fits-all score from a geocoding API would be wrong for most of your orders.
What confidence score threshold should I use to flag an order? There is no universal answer. Calibrate against your own historical data using the WEB batch tool to rescore past orders, then examine how confidence distributes across clean orders and chargebacks in your specific market. A threshold that works for a jewellery retailer shipping to dense urban postcodes will be wrong for a farm-supply retailer shipping to rural routes.
What does the match_type field actually mean? rooftop means the address resolved to a specific building or property location. street means it resolved to a position on the named street but not to a specific building. postcode means it only resolved to the postcode centroid — the address string did not match any known address and the engine fell back to the postcode boundary. The degradation from rooftop to postcode is the signal; a postcode-level match means the submitted address string is either non-existent or too malformed to resolve specifically.
How do I handle the geocoding call timing out on the checkout critical path? Set a tight timeout (4-5 seconds), catch the timeout exception, mark the geocoding signals as unavailable, and let the order through. Score the address asynchronously after the order is placed. A timeout is an infrastructure event, not a fraud signal.
Can I use these signals for non-US orders? The forward geocoding endpoint covers 63 countries and 504M+ addresses. Confidence scores and match types work globally. The IP geolocation is also global. The freight-forwarder watch list is yours and covers whatever geographies you maintain it for. The signals work internationally; calibrate your thresholds separately per market because address-data quality and VPN usage rates vary significantly by country.
How does this differ from the address validation post? Validating shipping addresses at checkout is about deliverability: does the address exist and will a carrier accept it? This post is about risk signals layered on top of the same geocoding calls: does the address resolve confidently, is it far from where the buyer's IP puts them, does the postcode appear in your freight-forwarder watch list? The deliverability check should ship first; the risk signals are a layer on top.
Does the WEB batch tool work for historical order rescoring? Yes. Upload a CSV of address rows and each row is geocoded and scored. Credits are consumed per address row. This is the practical way to calibrate your thresholds before setting them in production — score a year of historical orders, join back to known outcomes, and use the distribution to set thresholds empirically rather than by intuition.
Related Articles
- Geocoding confidence scores explained — what the confidence number actually measures and how to interpret degraded match types
- Add IP geolocation to your geocoding stack — the full integration pattern for combining IP location with address geocoding on the same API key
- Benchmarking geocoding APIs — honest numbers — how to evaluate geocoding coverage and match quality for your specific order geography
- Caching geocoding results — 90% cost reduction — addresses do not move; cache geocoding results at the checkout layer to eliminate redundant calls on repeat customers
- Exponential backoff — when to retry, when to stop — retry policy for the geocoding call on the checkout critical path and the async rescore job
---
*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 →