Skip to content

The 20-Minute Market — Opportunity Tiers

One sentence: for every Irish business category in every town, fcr_operations.v_opportunity_tier answers “is there already an established website competing inside this business’s real service radius?” — and therefore which rung of the product ladder (BizSite → SitePro / LocalRank, with SayMore as the moat-builder) is the honest recommendation.

All boundaries signed off 2026-06-11/12 (Cathal Dempsey). Numbers on this page are as of 2026-06-12; re-derive from the view rather than quoting this page for anything load-bearing.

Working with this in Claude Code: start with this page and scripts/opportunity-tier/README.md (the rebuild runbook). The full research record (plan, architecture, report, 16-slide deck, catchment viewer) lives in Cathal’s Present/ folder — ask for the pack. Project owner: Cathal Dempsey.

SELECT * FROM `listingmanager-1529856313699.fcr_operations.v_opportunity_tier`
WHERE canonical_key = 'plumber' AND locality = 'Clonmel'

One row per (locality, county, canonical_key), served at the category’s class minutes. Key columns:

ColumnMeaning
bucketTHIN / CONTESTED / SATURATED — the competitive verdict
market_class, pack_minutesdestination (20-min packs) or SAB service-area trade (30-min)
pack_n, pack_site25, pack_sitecomp, pack_dominator_reviewsthe pack evidence behind the verdict
home_n, home_no_sitebusinesses (and no-website businesses) based in this locality cell
confidentpack ≥ 5 — false rows must not drive automated recommendations
wide_radiuscounty-wide callout trades (drain_clearing, building_contractor) — verify THIN against rank data before pitching
desert_candidateTHIN + under 10K population at class minutes — no prize, excluded from BizSite lists
population, households, pp_weighted, pobal_weightedCSO context (purchasing power is ownership-weighted — NOT income)
hh_per_competitor, market_depththe prize grade: SHALLOW <1,000 / MID 1,000–3,000 / DEEP >3,000 households per pack member
  1. Never filter minutes = 20 — that silently drops every service-area trade. Filter market_class if needed.
  2. A missing row means ultra-THIN (market too small to measure), not “no data”.
  3. Respect confident = false — under 5 pack members is noise, not a verdict.
  4. Depth never moves the bucket — competition decides the verdict; depth grades the prize and orders the work.
  5. Exclude bucket classes (market_class = 'bucket': wholesale / manufacturing / nonmarket / other_*) and school from any opportunity count or targeting list.

The signed-off boundaries (and their evidence)

Section titled “The signed-off boundaries (and their evidence)”
BoundaryValueEvidenceSigned off
Market width (destination)20-minute drive-time catchmentWith/without-website outcome separation sharpest at 20 min (0.97× THIN vs 0.67× CONTESTED), gone at 45; 89% of visible top-3 rankers inside 20 min2026-06-11
Market width (SAB trades)30 minutesObserved ranks: SAB visible pack 75% in 20 min, 87% in 30 (9,094-search recovered scan). The decision holds, but the stated multiple was wrong: trades’ competitors reach ~1.4× further, not “~2× further” — re-measured 2026-07-13 on 1.68M ranked positions at a fixed lens (emergency_sab 9.4 km / scheduled_trades 9.1 km vs retail_errand 6.8 km). 30/20 = 1.5× time ≈ the measured 1.38× distance, so 30 minutes stands2026-06-12 · corrected 2026-07-13
BucketsTHIN = no established site (site + 25+ reviews) in pack; SATURATED = 3+ complete site-holders + 100+-review dominator; else CONTESTEDStep-2 outcome validation2026-06-11
Desert floorTHIN + population < 10K at class minutesRemoves ~410 of ~16.8K THIN (no-customers cases)2026-06-11
Depth bandsSHALLOW <1,000 / MID 1,000–3,000 / DEEP >3,000 households per pack memberRounded quartiles of the observed distribution (p25≈1,000 / p75≈3,300); re-derive with APPROX_QUANTILES(households/pack_n, 100)2026-06-12
Table (all in fcr_operations)What it is
drive_time_catchments1,601 calibrated drive-time polygons (Valhalla/OSM, calibrated vs 30 timed Google journeys; GEOGRAPHY)
category_bridgeall 4,479 raw category labels → 179 canonical markets (100% coverage). The supplies trap rule: trade-supplies sellers never map into the consumer trade
canonical_key_classper-market class (destination/SAB/bucket), pack minutes, wide_radius flags
opportunity_tier_lookupverdicts at every grain (20/30/45-min rows) — analysis only; consumers read the view
drive_time_catchment_demographicsCSO Census population / households / purchasing power per catchment
serp_grid_results109K observed ranking rows (May 2026 scans, competitor place_ids + premises GPS) — the reach evidence base
v_opportunity_tierthe one consumption surface — everything downstream reads this
  • Universe 228,832 → 226,856 with category → 187,835 in the analysis (39,021 deliberately excluded: 14,239 nonmarket, 10,989 other_*, 11,409 wholesale+manufacturing, 2,384 state schools).
  • No-website businesses: 96,482 total → 75,826 in confident measurable markets (+6,543 in pack<5 micro-markets, +14,113 with no measurable home cell — a geocoding gap that improves over time).
  • Opportunity (confident, non-bucket, non-school): BizSite/THIN 16,348 · LocalRank/CONTESTED 49,281 · SATURATED 9,787 · desert 410.
  • Depth grid of the THIN pool: DEEP 2,436 (the premium list) · MID 8,302 · SHALLOW 6,013.
  • Evidence on rankings (75K observed positions): 88% of the visible top-3 link a website; site-holders reach top-3 at 1.5× within the same search; ⚠ the “at home the website is a switch / reviews are the dial” MECHANISM IS CONTRADICTED — see Revalidation before quoting it; at home the website is a switch (15%→23–25% top-3, flat across review bands), away from home reviews are the dial (5.4%→9.1%) but proximity still wins (no-site local 15.1% beats remote site+100-reviews 9.1%).

Runbook with commands: scripts/opportunity-tier/README.md. In brief: re-run the calibration journeys (cents), re-run the with/without outcome measurement (cells grow past today’s 14–22), re-run the rank scan (~$25, diffs against the May base in serp_grid_resultsalways store full results, never rely on InSites’ copy: LRC reports return no competitor place_ids), extend the bridge for new labels (agent-batch pattern), then rebuild the lookup CTAS. The KEYWORD_INTELLIGENCE city/county backfill survives until the manual KI pipeline re-runs.

  • Three mis-geocoded localities excluded, pending human review: Crumlin/Antrim, Dundrum/Down, Kilnamanagh/Wexford (seeds landed on their Dublin namesakes).
  • Outcome-evidence cells are small (14–22 clients); directionally strong, re-measured quarterly.
  • Depth uses households as a demand proxy; the planned upgrade is the demand overlay (category search volume per area). Organic-reach domain-matching from the recovered scans is also unbuilt.
  • Engine wiring (worker/src/lib/opportunity-rules.js → the view) is the next build step; it awaits sales/product input on the per-class product benchmarks.
  • Counts are headcounts, not euros.

Re-measured against the 20-Minute-Market reach sweep: 1,676,469 ranked positions / 84,994 searches / 94,446 businesses / 120 categories × 694 localities, fired at the InSites-matched google_maps z13 lens (fcr_operations.serp_reach_observations_20260712). The original evidence came from a 9,094-search scan at a tighter zoom — that single fact explains most of what follows.

  • The 20-minute market boundary. Its evidence is outcome-based and therefore lens-free (no-website businesses earn 0.97× of site-holders’ Google actions in THIN markets vs 0.67× in CONTESTED; separation sharpest at 20 min, gone at 45). A search viewport cannot fabricate a client-outcome difference. This is the strongest evidence in the model and the sweep cannot overturn it.
  • The 30-minute SAB trades split. Trades genuinely reach further; only the stated multiple was wrong (below).
  • The website advantage (~1.5×). It survives — but not for the reason the deck gives (below).

1. “Trades’ competitors reach ~2× further.” Measured at a fixed lens: ~1.4×. The 30-minute decision it justifies is sound; the multiple is not. Corrected in the boundaries table above.

2. “The visible top-3 sits at median 2 km” — a LENS reading, not a market fact. The zoom pilot (plumber × 3 seeds × z12/13/14/15) shows the median distance to a top-3 ranker halves with every zoom step:

seedz12z13 (InSites)z14z15
city15.3 km8.9 km4.5 km1.8 km
town16.1 km8.4 km4.4 km6.5 km
rural13.7 km8.6 km6.9 km11.6 km

The deck’s 2 km is a z15 reading; the sweep’s 7.8 km is a z13 reading. They do not contradict — both are instrument readings.

LENS RULE — any distance drawn from a zoom-based search grid measures the viewport, not the market. Never quote a km radius from grid SERP data. Only two things survive: comparisons at a fixed lens (category vs category, town vs town), and outcome-based evidence. Prefer the InSites-matched lens (z13) — not because it is “true”, but because it is what the client sees in their own report.

3. The mechanism behind the website advantage is contradicted. Top-3 rate by review band on the identifiable population:

ReviewsNo websiteWith website
08.4%8.5%
1–2417.8%16.2%
25–9921.7%23.0%
100+22.1%25.1%

There is no website advantage below 25 reviews. Ranking tracks reviews. This is corroborated by Google’s own ranking documentation, which names prominence as “based on info like how many websites link to your business and how many reviews you have… more reviews and positive ratings can help your business’s local ranking — reviews are a named factor; owning a website is not.

So the slide-13 story (“at home the website is the SWITCH; reviews are the DIAL”) is backwards, and the sales script “reviews will not win you the local pack — the website will” is withdrawn pending re-measure.

🔍 Why the ~1.5× still holds — and what it really measures

Section titled “🔍 Why the ~1.5× still holds — and what it really measures”

Measuring the website effect only on businesses we can observe (those with a place_id) gives a feeble 1.15×. But that silently deletes the effect, because it drops the businesses missing from Google entirely — who are disproportionately the no-website ones. A 500-listing probe (searching unmatched goldenpages listings by name at their own coordinates; scripts/saymore-ads-reach/probe_lst_google_presence.py):

Has a Google place
With a website199/250 — 80%
Without a website149/250 — 60%

40% of no-website businesses have NO Google place at all — they can never rank. Weighting each arm by its probability of having a place restores ~1.45×, against the claimed 1.51×. Within noise.

The website’s real predictive power is that the business is on Google at all. Website and Google Business Profile travel together as a digital-presence bundle; the website is not ranking you, and reviews are.

For 40% of no-website prospects the honest opener is not “your competitors outrank you” — it is “you are not on the map at all.” Concrete, verifiable per prospect, and it points at presence management before a website.

  • The sweep covers 120 of the 178 canonical categories (694 localities — the same national spread).
  • The Google-presence probe is n=500 (250/arm) → roughly ±6 points at 95%. Enough to say 1.5× stands; not enough to publish 1.45× as a replacement number.
  • The review-band table pools home and away searches; slide 13 separates them. A like-for-like replication is blocked because PROSPECT_LISTINGS carries no lat/lng, so per-point distance cannot be computed for the non-appearing population. The mechanism is contradicted, but not yet re-measured on the exact construct.
  • SQL trap that hid this for one run: website LIKE 'http%' is NULL, not FALSE, when website IS NULL, so COUNTIF(NOT has_site) silently returns 0. Wrap in IFNULL(..., FALSE).
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?