Triaging catastrophe claims with geocoding after a storm

Geocode a CAT claims file in hours, map claim density, assign adjusters by drive time, and pull aerial snapshots for desk triage.

| July 29, 2026
Triaging catastrophe claims with geocoding after a storm

The event closes at 02:00. By 06:00 the claims inbox has ten thousand rows. By 08:00 the regional CAT manager wants a briefing map on her screen, adjuster territories carved out, and first-contact calls scheduled. By 09:00 field teams need to know which properties to drive to today and, ideally, what the roof looked like before they leave the staging car park.

This is not a hypothetical. It is what a competent claims operations team is expected to deliver in the hours after a severe weather event. The gap between what most carriers can actually produce in that window and what the operations director expects is large — and it is almost entirely a data-pipeline problem, not a staffing problem or an expertise problem.

The pipeline has four stages: geocode the claims file, visualise density, assign adjusters by proximity, and pull a pre-drive aerial snapshot per address. Every stage maps to a specific API surface. This post walks through each one in order, with working code, honest failure modes, and a clear picture of what the pipeline does not do — because overpromising in CAT response is how teams lose trust at exactly the wrong moment.

What the pipeline does not do

Say this to the CAT manager before the briefing, not after.

CSV2GEO provides geocoding, mapping, routing, and aerial imagery. It does not provide storm tracks, event footprints, damage probability models, or claims data. You supply the event boundary — your meteorological vendor or internal catastrophe model owns that layer. You supply the claims file — your policy management system owns that. This pipeline takes the address column from your claims file and makes it spatial. Everything downstream of that — which claims are inside the event footprint, what the expected damage quantum is, how to stage public adjusters — is your problem. The pipeline gives you the geographic primitives you need to think clearly about all of those questions.

If anyone in the briefing asks "which properties are confirmed damaged?", the honest answer is that the pipeline can show you which claims are clustered where and what those properties looked like before the event. Confirmed damage is a field inspection or a drone flight.

Stage one: geocoding the claims file

The claims CSV typically looks like this after extraction from the policy management system:

claim_id,policy_id,insured_name,address,city,state,zip,coverage_a,reported_at
CLM-000001,POL-88221,J. Hargreaves,"14 Maple Run Ct","Cedar Falls","TX",77082,310000,2026-07-29T03:14:00Z
CLM-000002,POL-91034,A. Vasquez,"209 Broadmoor Dr","Cedar Falls","TX",77083,195000,2026-07-29T03:51:00Z

What you need appended to every row is lat, lng, and a geocoding confidence score. The WEB batch tool accepts this CSV directly — credits are charged per address row, not per file upload. For programmatic ingestion in the CAT response pipeline, the REST endpoint gives you tighter control over error handling and lets you integrate with your existing job runner.

import csv
import os
import time
import requests

API = "https://csv2geo.com/api/v1"
KEY = os.environ["CSV2GEO_API_KEY"]
BATCH = 100  # sensible batch size for the geocoding endpoint

def geocode_claims(input_path, output_path):
    with open(input_path) as fin, open(output_path, "w", newline="") as fout:
        reader = csv.DictReader(fin)
        out_fields = reader.fieldnames + ["lat", "lng", "confidence", "geocode_status"]
        writer = csv.DictWriter(fout, fieldnames=out_fields)
        writer.writeheader()

        batch = []
        for row in reader:
            batch.append(row)
            if len(batch) >= BATCH:
                _flush(batch, writer)
                batch = []
        if batch:
            _flush(batch, writer)

def _flush(batch, writer):
    addresses = [
        "{address}, {city}, {state} {zip}".format(**r) for r in batch
    ]
    payload = {"addresses": addresses, "api_key": KEY}
    resp = requests.post(f"{API}/geocode/batch", json=payload, timeout=60)
    resp.raise_for_status()
    results = resp.json()["results"]
    for row, result in zip(batch, results):
        row["lat"] = result.get("lat")
        row["lng"] = result.get("lng")
        row["confidence"] = result.get("confidence")
        row["geocode_status"] = result.get("status", "ok")
        writer.writerow(row)
    time.sleep(0.1)  # polite pacing; tune to your rate limit tier

geocode_claims("claims_raw.csv", "claims_geocoded.csv")

The equivalent in Node if your CAT response tooling is JavaScript-based:

import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';

const API = 'https://csv2geo.com/api/v1';
const KEY = process.env.CSV2GEO_API_KEY;

async function geocodeBatch(addresses) {
  const r = await fetch(`${API}/geocode/batch`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ addresses, api_key: KEY }),
  });
  if (!r.ok) throw new Error(`geocode/batch returned ${r.status}`);
  const json = await r.json();
  return json.results;
}

Two things that matter during a CAT run when you are processing ten thousand rows at speed.

Watch the confidence score. Confidence below 0.7 means the geocoder could not confidently resolve the address — likely a rural route, a box number only, or a post-storm typographical error in the reported address. Flag those rows to a separate low_confidence_claims.csv for manual address verification. Do not plot them on the density map; they will create false cluster artefacts in areas where the geocoder defaulted to a ZIP centroid.

Honour 429s. The API returns HTTP 429 when you have hit your rate limit. Handle it with exponential backoff rather than a tight retry loop. Exponential Backoff — When to Retry, When to Stop has the exact retry budget arithmetic worth reading before you schedule a multi-thousand-row batch job. A well-written retry handler costs fifteen minutes; a misbehaving retry storm during a CAT event costs you your rate limit tier for the rest of the day.

Stage two: density map for the morning briefing

The CAT manager does not need a GIS application for the morning briefing. She needs a map image that she can drop into a slide deck and hand to the VP of Claims at 07:45. The Static Maps endpoint produces exactly that: a PNG or JPEG of a map tile with your data overlaid as labelled pins, no GIS tooling required, billed per image.

The pattern is: cluster the geocoded claims by a rough spatial grid (a simple lat/lng rounding approach works fine at the scale of a storm event), then call the Static Maps endpoint once per cluster with a pin and a count label. The morning briefing map is then the single highest-zoom-out image that shows every cluster.

import math
import requests
import os
from collections import defaultdict

API = "https://csv2geo.com/api/v1"
KEY = os.environ["CSV2GEO_API_KEY"]

def cluster_claims(geocoded_rows, grid_deg=0.05):
    """
    Round lat/lng to a grid of roughly 5 km cells at mid-latitudes.
    Returns dict of (grid_lat, grid_lng) -> list of rows.
    """
    clusters = defaultdict(list)
    for row in geocoded_rows:
        if not row["lat"]:
            continue
        grid_lat = round(float(row["lat"]) / grid_deg) * grid_deg
        grid_lng = round(float(row["lng"]) / grid_deg) * grid_deg
        clusters[(grid_lat, grid_lng)].append(row)
    return clusters

def build_briefing_map(clusters, output_path, width=1200, height=800):
    # Build pin list: top-15 clusters by claim count
    sorted_clusters = sorted(clusters.items(), key=lambda kv: -len(kv[1]))[:15]
    pins = []
    for (lat, lng), rows in sorted_clusters:
        count = len(rows)
        colour = "red" if count >= 50 else "orange" if count >= 20 else "yellow"
        pins.append(f"{lat},{lng},{colour},{count} claims")

    params = {
        "center": "auto",
        "zoom": "auto",
        "size": f"{width}x{height}",
        "pins": "|".join(pins),
        "api_key": KEY,
    }
    r = requests.get(f"{API}/staticmap", params=params, timeout=30)
    r.raise_for_status()
    with open(output_path, "wb") as f:
        f.write(r.content)
    print(f"Briefing map written to {output_path}")

A 15-cluster briefing map is one API call — one credit. The resulting PNG drops into a slide. The colour coding (red for 50+ claims, orange for 20-49, yellow below 20) is arbitrary; tune it to whatever threshold triggers a surge-staffing decision in your organisation. The labels show claim counts directly on the map so the VP of Claims can read the image without a legend.

For the briefing itself, generate three images: the full event footprint map, a zoomed-in view of the highest-density cluster, and a comparison against the policy book exposure map from your pre-event workflow (that pattern is described in a separate post on pre-event exposure mapping; this post is about post-event operations). Three Static Maps calls, three credits, five minutes of pipeline runtime. That is the material difference between a meeting where someone squints at a spreadsheet and one where someone says "right, the north-east quadrant is the priority, let us get fifteen adjusters there."

Stage three: adjuster assignment by drive time

Claim-density maps tell you where the work is. Drive-time routing tells you how to staff it. The routing matrix endpoint takes a set of adjuster home locations (or staging-depot coordinates) and a set of claim coordinates and returns the drive time from every adjuster to every claim. You then solve a basic assignment problem: assign each claim to the nearest available adjuster.

This is not the travelling salesman problem. You are not optimising a single tour. You are assigning a daily work queue to each adjuster such that their total drive time is minimised. The simplest version — greedy nearest-neighbour assignment — is good enough for a CAT morning briefing.

def assign_adjusters(adjuster_coords, claim_coords, max_claims_per_adjuster=12):
    """
    adjuster_coords: list of {"adjuster_id": str, "lat": float, "lng": float}
    claim_coords:    list of {"claim_id": str, "lat": float, "lng": float}
    Returns dict: adjuster_id -> [claim_id, ...]
    """
    origins = "|".join(f"{a['lat']},{a['lng']}" for a in adjuster_coords)
    destinations = "|".join(f"{c['lat']},{c['lng']}" for c in claim_coords)

    params = {
        "origins": origins,
        "destinations": destinations,
        "mode": "drive",
        "api_key": KEY,
    }
    r = requests.get(f"{API}/matrix", params=params, timeout=60)
    r.raise_for_status()
    matrix = r.json()["durations"]  # matrix[i][j] = seconds from adjuster i to claim j

    assignment = {a["adjuster_id"]: [] for a in adjuster_coords}
    claim_assigned = [False] * len(claim_coords)
    adjuster_load = [0] * len(adjuster_coords)

    # Greedy: repeatedly find the (adjuster, claim) pair with shortest drive time
    while True:
        best_time = float("inf")
        best_i = best_j = -1
        for i, adj in enumerate(adjuster_coords):
            if adjuster_load[i] >= max_claims_per_adjuster:
                continue
            for j in range(len(claim_coords)):
                if claim_assigned[j]:
                    continue
                t = matrix[i][j]
                if t < best_time:
                    best_time, best_i, best_j = t, i, j
        if best_i == -1:
            break  # all claims assigned or all adjusters at capacity
        assignment[adjuster_coords[best_i]["adjuster_id"]].append(
            claim_coords[best_j]["claim_id"]
        )
        claim_assigned[best_j] = True
        adjuster_load[best_i] += 1

    return assignment

The routing matrix is one API call regardless of how many adjuster-to-claim pairs you compute — the cost is one credit per call. Fifteen adjusters against two hundred claims (a reasonable CAT morning for a mid-size regional carrier) produces a 15×200 matrix in a single request. The greedy assignment loop runs in milliseconds on that size.

Three practical notes on the routing matrix in a CAT context.

Post-event road closures are not reflected. The routing matrix uses normal road network data. After a flood or tornado, roads get closed. Your adjusters will know about local closures on the ground; build a mechanism for them to reassign claims from a closed-route territory to an adjacent adjuster without having to phone the CAT desk and wait. A simple "reassign claim" button in the adjuster mobile app that posts to your back-end is enough.

Drive-time assignment ignores claim complexity. A hipped metal roof on a 4,000 sq ft property takes more time than a flat-roof single-storey. If your claims system has a coverage-A amount or a property-type code, weight the assignment to limit high-complexity claims per adjuster per day. The routing matrix gives you the geographic distance; the weighting is your business rule.

Capacity matters more than distance at the margins. An adjuster with two claims already assigned and a short drive beats an unloaded adjuster with a two-hour drive. The greedy algorithm above handles this via max_claims_per_adjuster. Set that number honestly — overloading an adjuster with twenty claims in one day does not close claims faster.

Stage four: per-address aerial snapshot for desk triage

Before an adjuster drives to a property, they should know what they are driving to. A 30-second aerial review per claim — what kind of roof, is there a detached structure, is there visible storm damage visible from above — lets the adjuster prepare the right equipment, calibrate their time estimate, and flag obvious total-loss candidates for the senior adjuster before leaving the depot.

The aerial image endpoint covers the contiguous United States plus Alaska, Hawaii, and Puerto Rico. This is US-only. For carriers writing policies in other territories, the aerial stage falls back to a manual-photo request from the policyholder. That fallback path needs to be built before the pipeline ships.

For US claims, the pattern is the one described in detail in Per-Policy Roof and Terrain Snapshots Without Satellite Licences. Here is the condensed form for the CAT pipeline context:

def fetch_aerial_snapshot(claim_id, lat, lng, out_dir="snapshots", size_m=350):
    os.makedirs(out_dir, exist_ok=True)
    r = requests.get(
        f"{API}/property/image",
        params={
            "lat": lat,
            "lng": lng,
            "size": size_m,
            "format": "jpg",
            "api_key": KEY,
        },
        timeout=30,
    )
    if r.status_code == 400:
        # Outside US coverage footprint.
        return None, "out_of_coverage"
    r.raise_for_status()
    path = os.path.join(out_dir, f"{claim_id}.jpg")
    with open(path, "wb") as f:
        f.write(r.content)
    return path, None

The same in Node for teams running a JavaScript claims operations backend:

import { writeFile, mkdir } from 'node:fs/promises';
import { join } from 'node:path';

const API = 'https://csv2geo.com/api/v1';
const KEY = process.env.CSV2GEO_API_KEY;

async function fetchAerialSnapshot(claimId, lat, lng, outDir = 'snapshots', sizeM = 350) {
  await mkdir(outDir, { recursive: true });
  const url = new URL(`${API}/property/image`);
  url.searchParams.set('lat', lat);
  url.searchParams.set('lng', lng);
  url.searchParams.set('size', sizeM);
  url.searchParams.set('format', 'jpg');
  url.searchParams.set('api_key', KEY);

  const r = await fetch(url.toString());
  if (r.status === 400) return { path: null, reason: 'out_of_coverage' };
  if (!r.ok) throw new Error(`property/image returned ${r.status}`);

  const buf = Buffer.from(await r.arrayBuffer());
  const path = join(outDir, `${claimId}.jpg`);
  await writeFile(path, buf);
  return { path, reason: null };
}

The image is cached server-side for 30 days. For a CAT event that generates claims over several days — as hurricane tail-end rainfall does — the same property address looked up twice in that window costs one credit on the first call and nothing on subsequent calls. For a renewal or reinspection workflow running in the same 30-day window, the cache absorbs the repeat lookups automatically. The broader caching argument is in Caching Geocoding Results — 90% Cost Reduction.

A point worth making explicitly: the aerial image was captured before the event. You are looking at pre-event roof condition. That is intentional — it is the baseline. It tells the adjuster what the property looked like when the policy was bound and what condition the insured was maintaining it in. Post-event aerial imagery (if your carrier subscribes to an event-response aerial program) is a separate data layer that you overlay on top of the pre-event image in the adjuster tool. Do not conflate the two.

Putting the stages together

The full CAT morning pipeline in outline:

Step 1: Ingest and geocode the claims file

Run the geocoding batch job against the raw claims CSV the moment it lands from the policy management system. Flag rows with confidence below 0.7 to a manual-verification queue. The output is claims_geocoded.csv with lat, lng, and confidence appended to every row. Ten thousand claims typically completes in well under twenty minutes depending on your rate limit tier.

Step 2: Build the briefing map

Cluster the geocoded claims to a 5 km grid, sort by density, and generate the Static Maps image. Three variants — full-event overview, highest-density cluster zoom, and a per-adjuster-territory breakdown — cover the standard morning briefing format. These are three API calls. Ship the PNG files to the shared drive or the briefing deck template fifteen minutes before the meeting.

Step 3: Run the routing matrix and produce adjuster assignments

Load your adjuster roster with home or staging-depot coordinates. Call the routing matrix endpoint once with all origins and destinations. Run the greedy assignment loop. Output a per-adjuster work queue CSV: adjuster_id,adjuster_name,claim_id,address,lat,lng,coverage_a,estimated_drive_time_s. This goes into the adjuster dispatch tool or is emailed directly depending on your operations setup.

Step 4: Pull aerial snapshots for the day's queue

For every claim in the day-one assignment list, fetch the aerial image. Do this concurrently with a sensible connection pool limit — ten concurrent requests is conservative and keeps you well below rate limits on any paid tier. Write each image to snapshots/{claim_id}.jpg. The adjuster dispatch tool attaches the image to the claim record so adjusters see it on their mobile device before driving.

Step 5: Ship the enriched file to your claims system

The final enriched row contains: claim_id, policy_id, insured_name, address, lat, lng, confidence, coverage_a, cluster_id, adjuster_id, adjuster_name, estimated_drive_time_s, aerial_image_url, geocode_status. Load this into the claims management system or the CAT response platform via your standard import path. The pipeline is complete.

Cost arithmetic for a real CAT event

A regional severe-weather event that generates 8,000 claims. Assume the free tier is not sufficient (3,000 calls per day is the free tier, and the aerial stage alone would exceed that) — you are on a paid plan.

| Stage | Calls | Notes | |---|---|---| | Geocoding | 8,000 | One per claim row | | Static Maps (briefing) | 3 | Three images for the morning briefing | | Routing matrix | 4 | One call per 50-adjuster region, four regions | | Aerial snapshots | 8,000 | One per claim; cache absorbs re-runs | | Total | ~16,007 | |

At paid pricing starting from $54/month for 100,000 calls, 16,000 calls is comfortably within a single month's budget even on the entry tier. For a carrier that writes 200,000 policies in a state and runs annual CAT drills, budgeting two to three months of paid API capacity for a major event is a reasonable planning number. The pricing page shows current bracket sizes.

The aerial image cache matters here. A two-week event where new claims arrive daily means many addresses were already imaged in the first 24 hours. If 40% of subsequent claims are at addresses already fetched in the same 30-day window, your effective aerial cost drops proportionally.

Observability during a live CAT run

Do not run a ten-thousand-row batch job during a live CAT event without instrumentation. The things that go wrong during a CAT batch run are predictable: geocode confidence anomalies on rural addresses, rate-limit exhaustion from a concurrent team also hitting the API, aerial out_of_coverage exceptions for properties that turn out to be outside the US coverage footprint.

Three metrics worth tracking in your APM tool during the run: geocoding success rate (rows with lat/lng populated divided by total rows — should be above 95%); aerial success rate (non-out_of_coverage responses divided by US-state claims rows — should be near 100% for contiguous US); API credit consumption rate (calls per minute — should stay below 60% of your tier limit to leave headroom for concurrent use). The full instrumentation argument is in Observability for Geocoding Pipelines.

If the geocoding success rate drops below 90%, stop the batch and investigate before continuing. A 10% miss rate on ten thousand claims is a thousand addresses that will not appear on the density map or in the adjuster queue — that is a claims-cycle problem, not just a data-quality footnote.

Frequently Asked Questions

Does CSV2GEO provide the event footprint or damage model?

No. CSV2GEO provides geocoding, mapping, routing, and aerial imagery. Your catastrophe model, meteorological data vendor, or internal risk team supplies the event boundary and damage estimates. This pipeline makes the claims file spatial; the event-footprint overlay is your layer.

Is the aerial imagery post-event imagery showing storm damage?

No. The aerial endpoint returns pre-event imagery from a public-domain federal aerial program flown on a multi-year cycle. It shows the property's pre-event condition. Post-event aerial imagery, if your carrier subscribes to an event-response capture program, is a separate data layer. The pre-event baseline is still valuable — it shows roof condition at policy inception and the insured's maintenance history.

What happens to claims for properties outside the US?

The aerial image endpoint covers the contiguous US plus Alaska, Hawaii, and Puerto Rico. For properties outside that footprint, the endpoint returns HTTP 400. Build a fallback path — typically a policyholder self-upload request — before the pipeline ships. Geocoding and routing work globally; only aerial imagery is US-only.

How do I handle addresses that geocode with low confidence?

Flag any row with confidence below 0.7 to a manual-verification queue. These are likely rural routes, box numbers, or transcription errors in the claim report. Do not plot them on the density map; ZIP-centroid geocodes create false cluster artefacts. A human address-verification step on the low-confidence queue typically resolves 80%+ of those rows within an hour.

Can the routing matrix handle post-event road closures?

The routing matrix uses the normal road network and does not reflect real-time or post-event road closures. Your adjusters will encounter closures in the field. Build a mechanism in your adjuster mobile app for them to reassign claims to an adjacent territory without phoning the CAT desk — a simple API call to your back-end that updates the assignment table is sufficient.

How much does the full pipeline cost per claim?

Approximately two credits per claim for a standard run: one geocoding call, one aerial image (routing matrix is charged per call rather than per claim, so it amortises across the full event). At paid pricing starting from $54/month for 100,000 calls, the marginal cost per claim is under $0.002. Aerial cache hits during multi-day events reduce the effective cost further.

Does the pipeline work for wildfire claims as well as storm claims?

Yes, with one meaningful difference: wildfire events tend to produce total-loss clusters in concentrated areas, and the aerial image may show a structure that no longer exists if the flight cycle predates the fire. For wildfire events, the pre-event aerial is still the correct baseline for replacement-cost validation; supplement it with post-fire satellite or drone imagery from a specialist provider if your claims process requires confirmed post-event condition assessment.

Related Articles

---

*I.A. / CSV2GEO Creator*

Ready to geocode your addresses?

Use our batch geocoding tool to convert thousands of addresses to coordinates in minutes. Start with 100 free addresses.

Try Batch Geocoding Free →