← All posts Retail

Score 12 candidate retail sites before lunch

A site-selection director ranks 12 candidate Tampa locations on drive-time demographics and competitor density. Nine steps, about three minutes of wall clock.

Site selection is an exercise in narrowing a long list. You start with 50 candidates, narrow to 12 finalists, narrow to 3 for in-person tours, sign one lease. The bottleneck is rarely "do we know what good looks like?" — it's "can we apply our criteria across 12 candidates fast enough to keep the deal warm?"

The traditional answer involved Placer.ai or SafeGraph licenses ($30K+/yr), an analyst who knew ArcGIS, and a 3-day turnaround. This walkthrough shows the modern version: nine chained operations, no GIS team, and a scored mappable shortlist at the end.

The scenario

A CPG-adjacent retail concept (premium coffee, neighborhood gym, fast-casual — pick your favorite) wants to expand into Tampa. Internal criteria:

The director has a CSV of 12 candidate site addresses and a CSV of competitor locations. Their agent has Maps MCP wired in. Here's the chat.

Step 1 — Geocode the candidates

{
  "name": "bulk_match_addresses",
  "arguments": {
    "records": [
      { "id": "site-01", "street": "...", "city": "Tampa", "state": "FL" },
      { "id": "site-02", "street": "...", "city": "Tampa", "state": "FL" }
      // ... 10 more
    ]
  }
}

One call, 12 lat/lng pairs. Cost: free (U.S. Census geocoder).

Step 2 — Trade-area polygons (drive-time isochrones)

For each site, we want a 10-minute drive polygon — the actual reachable footprint, not a Euclidean circle.

{
  "name": "geo_isochrone",
  "arguments": {
    "centers": [/* 12 lat/lng from step 1 */],
    "mode": "drive",
    "minutes": 10
  }
}

12 polygons. These are what the rest of the analysis joins against.

Step 3 — Competitor count per trade area

Parse the competitor CSV in your client, then ingest the records into a workspace dataset and intersect each trade-area polygon against it:

{
  "name": "ingest_dataset",
  "arguments": {
    "slug": "competitors_tampa",
    "name": "Competitor locations — Tampa MSA",
    "collection": "us-zcta",
    "version": "tiger-2024",
    "records": [
      { "external_id": "33602", "data": { "brand": "X", "lat": 27.95, "lng": -82.46 } }
      // ... one record per competitor, joined to the containing ZCTA
    ]
  }
}

# Then for each of the 12 trade-area polygons:
{
  "name": "geo_intersect_dataset",
  "arguments": {
    "dataset": "competitors_tampa",
    "geometry": { "type": "Polygon", "coordinates": [/* one of the 12 isochrones */] }
  }
}

For each of the 12 trade areas, we now know how many competitors are inside it. Filter rule: drop any site with a competitor < 1 mile (which translates to ">0 competitors in the 10-min drive polygon — close enough at this scope").

Step 4 — ACS demographics per trade area

Pull median household income (B19013), population (B01003), and median age (B01002) for the ZCTAs inside each trade area:

{
  "name": "query_shapes_by_intersection",
  "arguments": { "polygons": "trade_areas_step_2", "collection": "us-zcta" }
}

{
  "name": "census_acs",
  "arguments": {
    "geo": "us-zcta",
    "tables": ["B19013", "B01003", "B01002"],
    "ids": "zcta_ids_step_4a"
  }
}

For each trade area, the agent computes a population-weighted average of median income and density. (This is the kind of arithmetic any LLM does cleanly given the input rows — no notebook needed.)

Step 5 — Score, rank, and render

The agent scores each of the 12 sites against the four criteria, ranks them, and renders a map:

{
  "name": "create_report",
  "arguments": {
    "slug": "tampa-12-sites",
    "name": "Tampa site selection — 12 candidates scored",
    "is_public": false,
    "config": {
      "layer": {
        "polygons": "trade_areas_step_2",
        "points": "candidates_geocoded",
        "color_by": "score",
        "tooltip": ["score", "median_hhi", "density", "competitors"]
      }
    }
  }
}

Internal-only report (public: false). The report has a private URL that only the workspace can see. Pro tier can lock embed iframes to a specific domain via config.embed_origins.

Step 6 — Have Claude summarize

{
  "name": "describe_report",
  "arguments": { "report": "tampa-12-sites" }
}

The response is a prose summary keyed off the rendered values: which sites pass all criteria, which fail each one, where the income-vs-competition trade-offs sit. Useful as a starting point for the deck — re-runnable when criteria change. The output is grounded in the actual report data, so the numbers cited line up with what the iframe shows.

What this is and isn't

Is: a fast first pass that turns 12 candidates into 3 finalists using uniform criteria. Same job a junior analyst used to spend 2 days on, now ~3 minutes of wall clock.

Isn't: a substitute for the in-person tour, the parking-lot count, or the broker conversation. It's the screening pass before those things, not a replacement.

What you'd add next

Try it yourself

Free tier — 50 calls / month, no card.

Get an API key