Skip to content

Maps & Local Rank Check (LRC)

The FCR-built maps subsystem: the rank-check grid, the local-insights map, and how the three map flows (LRC, Dashboard Discovery, Prospect) differ — plus which external service supplies each data point. All maps are served by the Worker as self-contained HTML (Leaflet) and shared via /r/:id. This is FCR’s own LRC — separate from InSites/SayMore’s LRC.

For the client-facing “how to read the report” explainer see lrc-methodology.md. Grid-sizing maths: LRC_Grid_Sizing_Brief.md. Verified 2026-05-20.


There are two “LRC”s — don’t conflate them:

  • FCR LRC (this doc) — our own rank check, computed by querying SerpAPI at a grid of points. Canonical. Lives in the Worker (serp-grid, grid-map-build). Predictable credit cost, real per-point coordinates.
  • InSites LRC — SayMore’s third-party rank check (submitted via InSites). Used for the home anchor grid historically; being sunset in favour of SerpAPI as the canonical source.

dashboard-serp-grid is the engine:

  1. Take a centre lat/lng, a size (e.g. 5 → 5×5 = 25 points), and a spacing_km. Generate a size × size grid of real coordinates.
  2. For each point × each keyword, query SerpAPI engine=google_maps and read the top-20 local pack results.
  3. Return the per-point results in an InSites-LRC-compatible shape, so the map builder treats SerpAPI and InSites grids uniformly.
  4. The prospect’s rank at each point = its position in that point’s local pack (or “not in top 20”). Across the grid that’s the heatmap of where they rank well vs poorly.

Key compute details:

  • Zoom is auto-derived from spacing (not hardcoded). A wide zoom let city-centre giants (e.g. “Temple Bar Pub”) rank #1 at every point and buried the actual prospect — tighter zoom = genuinely local results. (Fixed 2026-04-26.)
  • Irish-leak guard: each SerpAPI response is validated for Irish signals (county names, eircode) and retried once with a tighter location string if it drifts to US results.
  • Geometry (lib/grid-geometry.js): coverage_km per grid + satellite overlap analysis, so multi-grid layouts don’t double-cover the same area (the check_satellite_overlap advisor tool uses this before burning SerpAPI).
  • Catchment radius by place type (metro vs regional, Place vs SAB) is set in grid-map-build.js — see reference notes; 12/20/25/50 km tiers.

grid-map-build then assembles the shareable map from the grid + the other layers (below) and renders the /r/:id HTML with four sidebar tabs: Rankings · Demographics · Competitors · Web Results.

Web Results population. The “Web Results” tab is fetched (SerpAPI google

  • screenshots) when the build has a Discovery overlay, passes include_insights (the default for prospect / shared LRC publishes), or sets include_web_results. It is decoupled from the insights narrative — a published map still gets Web Results even if the AI narrative failed to generate. Lean “quick internal” maps (include_insights=false, no flag) stay lean and skip the SerpAPI + screenshot cost. Caveat: this is the worker JS renderer only. The /prospect skill’s Python renderer (build_lrc_grid_map.py) does not render Web Results (or posture preset / regenerate). Closing that gap is the planned consolidation: route /prospect through dashboard-grid-map-build and retire the Python renderer (verify the worker handles the multi-grid home+satellite shape first).

All three produce a Leaflet map; they differ in who drives it and how much is automated.

FlowWhat it isEntryDriver
LRCThe rank-check grid map itself — pick business + keywords, get the heatmap + layers, share /r/:iddashboard-grid-map-build / dashboard-serp-gridthe primitive everything else builds on
Dashboard Discovery (Alpha)A stepped, AI-guided build of an LRC report inside the dashboard: suggest keywords/area → run grid → AI insights → DF competitor compare, all baked into one shareable mapdashboard-discovery-suggest-insights-df-comparethe AM, interactively in the dashboard
ProspectMeeting-prep, multi-grid LRC: a home grid + satellite grids (SAB centre override, shared + geo keywords), overlap-checked, as a customer-facing pitch artefactthe /prospect skill (Roam / CLI)the AM prepping for a meeting

In short: LRC is the map primitive; Discovery is the guided in-dashboard builder around it; Prospect is the multi-grid meeting-prep flow.


4. Which service supplies which data point

Section titled “4. Which service supplies which data point”

The maps call several services — one per data point:

Data pointServiceWorker route / source
Local rankings (the grid heatmap)SerpAPI google_maps (local pack)dashboard-serp-grid
Web SERP results (the “Web Results” tab — mobile google.ie organic)SerpAPI google + screenshotdashboard-serp-local, dashboard-serp-screenshot
Place search / lat-lng / NAPGoogle Maps via SerpAPIdashboard-prospect-search
GBP profile (reviews, categories, hours, photos)Pleper (+ SerpAPI gbp-live)dashboard-gbp-live, used in grid-map-build + discovery-insights
Demographics + Purchasing PowerCSO (cso_small_areas_v2, census)dashboard-circle-demographics, bbox-demographics
Online reachFB / IG (Meta) ad-audience estimatedashboard-meta-reach (+ -batch)
Keywords (what to rank-check, volumes, intent)FCR Keyword Knowledge BaseKEYWORD_INTELLIGENCE, dashboard-category-keywords
Map AI commentaryClaudedashboard-map-advisor

So a single LRC/Discovery map can fan out to SerpAPI (ranks + web), Pleper (GBP), Google Maps (places), CSO (demographics/PP), Meta (reach), and the Keyword KB (keywords) — each layer from its own source.


  • Local insights map (Area Intelligence) — dashboard-area-map: the layers map of an area (purchasing power, population density, housing tenure, commercial properties, deprivation, Meta reach). Sourced from CSO + Meta. Standalone, or as the Demographics tab on an LRC map.
  • “Quick — who ranks locally?” — the light competitor path (~3-4s, NOT a full grid): search_google_places (category + locality) → enrich_competitors. Returns competitors with reviews/hours/category breadth + threat level, without burning a SerpAPI grid. Used in the HubSpot card / Roam for a fast answer; escalate to run_serp_grid only when the AM asks for actual rank/SERP.
  • Identify key competitorsdashboard-competitor-enrich / enrich_competitors: for a set of place_ids, adds services, photo count, response rate, hours, all GBP categories, and fcr_link (whether each is already on FCR’s books) — surfacing a natural competitor to benchmark against and cross-sell from.

  • All maps are Worker-served HTML (Leaflet from CDN, data inlined) — no separate front-end build. The dashboard UI embeds them; clients open them via the public /r/:id share URL.
  • The published map message + the four-tab explainer is what the advisor surfaces when it publish_grid_maps (see the advisor tooling).
  • Client-facing methodology (how ranks are measured, how to read each layer): lrc-methodology.md — served at dashboard-lrc-methodology.

7. Known issue — duplicate publish_grid_map on fresh LRC intents (open, 2026-05-27)

Section titled “7. Known issue — duplicate publish_grid_map on fresh LRC intents (open, 2026-05-27)”

Symptom. A single-grid LRC (“who ranks for florists in clontarf”) through the Roam advisor took 4m 02s end-to-end vs ~2m 30s for an equivalent single-grid LRC the week prior (Fergal Colbert / Dungarvan, same shape: 5×5 grid, 5 keywords, 4 advertised tool calls).

Diagnosis. Worker-side advisor_usage_log for the slow run (n8n exec 37234) shows the tool sequence was:

run_serp_grid, enrich_competitors, publish_grid_map, publish_grid_map

publish_grid_map ran twice. Comparing to the 2m 30s Fergal baseline (n8n exec 36494) — same intent, same grid shape, more competitors enriched (13 vs 7) — that run published once. The extra publish + the extra Claude round-trip needed to reach it accounts for ~75–85 s of wall clock, which matches the 85 s gap almost exactly. Output tokens were also ~85% higher on the slow run, consistent with the model regenerating its response after the second tool call.

Root cause. Two paragraphs in worker/src/handlers/ai-advisor.js give the model contradictory defaults for include_insights:

  • L4690 (GRID MAP EXPORT block): “DEFAULT TO LRC (include_insights OFF) … plain ‘LRC’ / ‘run a map’ / ‘/prospect’ stays tables-only.”
  • L4700 (WHEN TO AUTO-FIRE, 10 lines below): “After enrich_competitors returns … call publish_grid_map immediately with grid_labels …, competitors_enrichment …, AND include_insights=true.”
  • L4701: “If enrich was skipped (explicit spot-check), still auto-publish with include_insights=true unless the AM said ‘just chat’ / ‘no map’.”

So the same block tells the model both “LRC default = insights OFF” and “auto-fire = insights ON”. On the florist run the model resolved this by calling publish_grid_map once for each interpretation. Only the first URL was surfaced to the AM (the closing footer in the response — “Say ‘add insights’ to republish with the AI analysis” — is the LRC-default footer copy), so the second publish was effectively waste.

Mid-flow runs (e.g. when the AM replies "a" to a /prospect proposal) don’t re-derive include_insights from scratch and don’t trip this — which is why the Fergal baseline was clean.

Fix options (not yet applied — flag for prompt owner):

  1. Reconcile the two paragraphs. Pick a single default for the auto-fire path. Either drop the AND include_insights=true clauses at L4700–L4701 (LRC-default wins, matches L4690 + the user-facing “say ‘add insights’ to republish” footer), or revert L4690 (insights-default wins) and update the footer copy elsewhere. The first option matches current product behaviour; whichever is chosen, the two paragraphs must agree.
  2. Idempotent publish in the handler. In publish_grid_map’s branch (ai-advisor.js, ~L3504), short-circuit a second call with the same (sessionKey, business_name, grid_labels, include_insights) within e.g. 60 s by returning the cached result without re-invoking dashboard-grid-map-build. This is the cheap safety net and defends against any future prompt drift.

(1) addresses the root cause; (2) caps the worst case at ~10 s instead of ~80 s if the model still double-calls for any other reason. Worth doing both.

Repro evidence.

RunDateUser msgTool sequenceWall clockWorker latencyOutput tokens
Slow (today)2026-05-27”who ranks for florists in clontarf”run_serp_grid, enrich_competitors, publish_grid_map, publish_grid_map4m 02s237 s5,565
Baseline2026-05-25”a” (mid /prospect flow) — Fergal Colbert LRCsearch_google_places, run_serp_grid, enrich_competitors, publish_grid_map2m 36s152 s3,009
Heavier (3-grid)2026-05-25”3 seperate” — DS Electrical home + 2 satellitessearch_google_places, check_satellite_overlap, run_serp_grid×3, enrich_competitors, publish_grid_map4m 14s249 s3,719

The 3-grid run did 3× the SERP work for the same wall time as today’s single-grid run — confirming the regression is in the orchestration layer (duplicate publish + longer model response), not in SerpAPI / Places / Claude API latency.

Ask the docsRAG over this site
Ask anything about the FCR Dashboard platform — architecture, BigQuery, the worker routes, billing rules, the LRC stack, scoring… Answers are grounded in this documentation, with source links.
How does the deal-brief refresh work? Which routes are Worker vs n8n? How is account health scored?