map-scout: analyze a place, build ranked Maps lists
Pipeline: Scope -> Longlist -> Shortlist research -> Scoring -> Review digest -> Maps build -> Verify -> Report.
The complete browser mechanics (Playwright over CDP, selectors, save idempotency, notes, list deletion, pitfalls) live in the google-maps skill: read it before the Maps build and follow it exactly; nothing from it is duplicated here.
Phase 0: scope + pre-flight
Clarify scope (derive from the prompt, ask only on real ambiguity):
- Area: city / region / radius. "around X" without a number = 15 km radius.
- Place type (big city / small town / beach or nature region) determines the flex category.
- Master file location: the user's trip or project folder. If none exists, search the workspace for one first (avoid duplicates); if nothing fits, ask the user where.
Pre-flight, check everything NOW, not after 2 hours of research:
- CDP Chrome reachable?
http://127.0.0.1:9223/json/version (port 9223 by default; 9222 usually belongs to other CDP tooling). If not: google-maps/scripts/launch-chrome-cdp.ps1 (does its own port collision check, google-maps section 2).
- Account check per google-maps section 3a (mandatory; the profile can hold several accounts). Mismatch = STOP.
python3 -c "import playwright" works. (Multi-line Python never inline in a shell; always a script file.)
Categories: default 5 + flex slot
| # |
List |
Emoji |
Content |
| 1 |
Restaurants |
🍝 |
dinner / main meals |
| 2 |
Cafes & Breakfast |
☕ |
brunch, breakfast, specialty coffee |
| 3 |
Sights |
🏛️ |
landmarks, museums, old town, viewpoints |
| 4 |
Activities & Experiences |
🚗 |
tours, outdoor, markets, day trips within the radius |
| 5 |
Flex slot |
|
big city: 🍸 Bars & Evening · beach/nature: 🏖️ Beaches & Nature Spots · real food city: 🍦 Street Food & Snacks |
- A list only with 5 or more valid candidates; never pad with mediocrity (drop the list instead and say why in the report).
- Max 10 places per list, max 5 category lists plus the ⭐ list.
- A place lands in exactly one category list (the best fit); the only exception is the ⭐ list. If it fits two, mention that in the report.
Research: two stages (token and time budget!)
Hard rule from google-maps: Playwright never for research, only for the Maps UI. Research runs through the agent's web search, web fetch, or a browser MCP. Close any browser tabs you opened when done.
Stage 1, longlist (cheap): per category 15-25 candidates ONLY from Google Maps search results (browser MCP or web fetch): name, stars, review count. 1-2 searches per category are enough.
Entry gate into the pool: a Maps entry exists AND (25+ Maps reviews OR 2+ independent blog sources). This keeps ghost venues and the 5.0-stars-with-8-reviews trap out.
Stage 2, shortlist (expensive, only the top ~12 per category by Maps score):
- Capture an anchor (mandatory): phone OR street+number per candidate. Every cross-source match runs over the anchor, never over name similarity (matching "Ramiro" to "Cervejaria Ramiro" by name is guessing; the anchor decides). The anchor later doubles as the pin validation (google-maps 3d).
- TripAdvisor, access cascade (a direct fetch is Cloudflare luck, do not get stuck on it): (a) web search
site:tripadvisor.com <place> <category>, the snippets usually contain "#X of Y" plus the rating; (b) fetch the page; (c) browser MCP.
- Blogs & guides (web search, aggregate per category, not per place): two-source rule. Independent = different operators, visibly visited themselves (own photos, concrete anecdotes, names weaknesses), named author. An established editorial guide (Michelin, Lonely Planet, Eater, The Infatuation, Time Out) counts as 2 blogs. Does not count: SEO listicles without visit evidence, affiliate farms, copied place lists.
All raw data (rating, review count, TA rank/rating, blog sources with URL, anchor) goes into the master file immediately.
Scoring
Subscores per source on a 0-10 scale:
- Maps:
R_adj = (v/(v+m))*R + (m/(v+m))*C with R = stars, v = review count, m = median review count in the category pool, C = pool average. Then clamp((R_adj - 4.0) * 10, 0, 10).
- TripAdvisor: rank position
(1 - rank/total) * 10. Ranks are ONLY comparable within the same TA locality pool; for a radius/region spanning several localities, run the TA rating through the Maps formula instead for ALL candidates (reuse m and C from the Maps pool, v = TA review count). No mixed scales: per category uniformly rank OR the rating fallback; a single place without a rank is imputed like a missing TA source.
- Blogs: 0 mentions -> 0 · 1 -> 4 · 2 -> 7 · 3+ -> 10. 0 mentions is a regular score, not a "missing source".
- Only TA can be missing (Maps is the entry gate, blog 0 counts as a score): impute the TA subscore conservatively with the 25th percentile of the pool's TA subscores. No renormalizing, no penalty.
Fairness step (this is what makes the thirds real): per category convert every subscore into a 0-10 percentile rank over the candidate pool (ties = average rank, scaled linearly to 0-10; "pool" = the category's shortlist, while m/C come from the longlist). Total = mean of the three percentile scores. Reason: raw spreads are unequal (Maps clusters tightly after damping, blogs jump in steps); without the alignment the blog steps dominate the ranking and "one third each" would be a lie.
The score decides, but not blindly: on a stark source contradiction (Maps loves it, blogs warn of a tourist trap) read the reviews and decide with a reason; the decision goes into the master file.
Review digest (for every list entry, mandatory)
Token-friendly, no full scroll: (a) the Maps review summary / keyword chips on the place page, (b) skim the 3-5 most relevant reviews, (c) deliberately 1-2 of the most critical negative reviews (deal-breaker check: scam? hygiene? tourists-only?). For the top 3 of every list additionally cross-check the already collected TA/blog quotes.
Result per place: max 6 sentences, compact and straight to the point, no filler. Pattern: what genuinely makes the place special (review essence), 1 concrete practical tip (what to order / when to go / book ahead), a warning if one exists, the closing-day constraint, phone. This text becomes the note 1:1.
Maps build (engine: google-maps 3.0, do not script it yourself)
- FIRST finish the master file completely (ranked lists, final note texts, sources, anchors). It is the source of truth; the build afterwards is pure transfer.
- Generate
data.json in the engine format from it (google-maps 3.0): per category {"list": "<emoji> <place> · <category>", "emoji": ..., "places": [...]}, places in rank order (#1 first); the engine reverses on its own, does anchor validation, order auto-repair, notes after the final order, and the end verify.
- Note per place (identical in master file and data.json): prefix
🥇/🥈/🥉 for the top 3, 4 · to 10 · for the rest, then the review digest (max 6 sentences). The style convention in google-maps 3e applies hard (no markdown, no address duplication, no filler).
- ⭐ list
⭐ <place> · Top Picks as the extra entry in data.json: the top 3 of every category, notes here 🥇 Restaurants · <one-liner why>. The user can then switch the category lists off and only ⭐ on: only the best on the map.
- Build:
python3 ~/.claude/skills/google-maps/scripts/maps_lists.py build data.json. Exit 0 means verified. A crash rerun is safe (existence check and aria-checked idempotency are built in). Afterwards spot-read 2-3 notes.
Report
- Per list the top 3 with a one-liner and score, below that briefly what narrowly missed and why (reasons are mandatory, never just the final list).
- Name the master file path. Share links optional (google-maps 3f; on dropdown flakiness hand it to the user as a manual step).
Failure playbook
| Problem |
Reaction |
| Crash mid-build |
Rerun: the master file is the source of truth, the 3b/3d anchors prevent duplicates and toggle accidents |
| Session/token budget runs low |
Save the master file state immediately, make a clean cut, build in a follow-up session (a rerun is cheap) |
| TripAdvisor blocks completely |
Work the cascade; if it yields NOTHING, drop the TA dimension (total = mean of Maps and blog percentiles) and say so in the report |
| A selector breaks |
Check google-maps section 6 pitfalls; the fix belongs in the engine maps_lists.py plus a section 6 row, no parallel script |
| UI structure unknown/changed |
Do not guess: DOM probe (dump aria-labels/roles), add the finding to google-maps section 6 |
1---2name: map-scout3description: Analyze a city, region, or radius ("15 km around X") and build ranked Google Maps Saved Lists of the best places: restaurants, cafes and breakfast, sights, activities, bars. Research from three source families (Google Maps + TripAdvisor + independent blogs, one third weight each; if TripAdvisor blocks entirely, the other two carry half each), Bayesian rating adjustment, percentile scoring, top 3 per list marked 🥇🥈🥉 plus a ⭐ top-picks list. Use on "scout <place>", "analyze <place> for our trip", "best restaurants/cafes in <place>", "what should we eat or do in <destination>", and any vacation prep that should end in Maps lists, even when the word "Maps" never appears. NOT for flights, hotels, or route planning, and not for pure Maps UI mechanics without research (that is the google-maps skill).4---56# map-scout: analyze a place, build ranked Maps lists78Pipeline: **Scope -> Longlist -> Shortlist research -> Scoring -> Review digest -> Maps build -> Verify -> Report.**9The complete browser mechanics (Playwright over CDP, selectors, save idempotency, notes, list deletion, pitfalls) live in the **`google-maps`** skill: read it before the Maps build and follow it exactly; nothing from it is duplicated here.1011## Phase 0: scope + pre-flight1213**Clarify scope** (derive from the prompt, ask only on real ambiguity):14- Area: city / region / radius. "around X" without a number = 15 km radius.15- Place type (big city / small town / beach or nature region) determines the flex category.16- Master file location: the user's trip or project folder. If none exists, search the workspace for one first (avoid duplicates); if nothing fits, ask the user where.1718**Pre-flight, check everything NOW, not after 2 hours of research:**191. CDP Chrome reachable? `http://127.0.0.1:9223/json/version` (port 9223 by default; 9222 usually belongs to other CDP tooling). If not: `google-maps/scripts/launch-chrome-cdp.ps1` (does its own port collision check, google-maps section 2).202. Account check per google-maps section 3a (mandatory; the profile can hold several accounts). Mismatch = STOP.213. `python3 -c "import playwright"` works. (Multi-line Python never inline in a shell; always a script file.)2223## Categories: default 5 + flex slot2425| # | List | Emoji | Content |26|---|---|---|---|27| 1 | Restaurants | 🍝 | dinner / main meals |28| 2 | Cafes & Breakfast | ☕ | brunch, breakfast, specialty coffee |29| 3 | Sights | 🏛️ | landmarks, museums, old town, viewpoints |30| 4 | Activities & Experiences | 🚗 | tours, outdoor, markets, day trips within the radius |31| 5 | Flex slot | | big city: 🍸 Bars & Evening · beach/nature: 🏖️ Beaches & Nature Spots · real food city: 🍦 Street Food & Snacks |3233- A list only with **5 or more valid candidates**; never pad with mediocrity (drop the list instead and say why in the report).34- Max **10 places per list**, max **5 category lists** plus the ⭐ list.35- A place lands in exactly **one** category list (the best fit); the only exception is the ⭐ list. If it fits two, mention that in the report.3637## Research: two stages (token and time budget!)3839Hard rule from `google-maps`: **Playwright never for research**, only for the Maps UI. Research runs through the agent's web search, web fetch, or a browser MCP. Close any browser tabs you opened when done.4041**Stage 1, longlist (cheap):** per category 15-25 candidates ONLY from Google Maps search results (browser MCP or web fetch): name, stars, review count. 1-2 searches per category are enough.4243**Entry gate into the pool:** a Maps entry exists AND (25+ Maps reviews OR 2+ independent blog sources). This keeps ghost venues and the 5.0-stars-with-8-reviews trap out.4445**Stage 2, shortlist (expensive, only the top ~12 per category by Maps score):**46- **Capture an anchor (mandatory):** phone OR street+number per candidate. Every cross-source match runs over the anchor, never over name similarity (matching "Ramiro" to "Cervejaria Ramiro" by name is guessing; the anchor decides). The anchor later doubles as the pin validation (google-maps 3d).47- **TripAdvisor, access cascade** (a direct fetch is Cloudflare luck, do not get stuck on it): (a) web search `site:tripadvisor.com <place> <category>`, the snippets usually contain "#X of Y" plus the rating; (b) fetch the page; (c) browser MCP.48- **Blogs & guides** (web search, aggregate per category, not per place): two-source rule. Independent = different operators, visibly visited themselves (own photos, concrete anecdotes, names weaknesses), named author. An established editorial guide (Michelin, Lonely Planet, Eater, The Infatuation, Time Out) counts as 2 blogs. Does not count: SEO listicles without visit evidence, affiliate farms, copied place lists.4950All raw data (rating, review count, TA rank/rating, blog sources with URL, anchor) goes into the master file immediately.5152## Scoring5354Subscores per source on a 0-10 scale:55- **Maps:** `R_adj = (v/(v+m))*R + (m/(v+m))*C` with R = stars, v = review count, m = median review count in the category pool, C = pool average. Then `clamp((R_adj - 4.0) * 10, 0, 10)`.56- **TripAdvisor:** rank position `(1 - rank/total) * 10`. Ranks are ONLY comparable within the same TA locality pool; for a radius/region spanning several localities, run the TA rating through the Maps formula instead for ALL candidates (reuse m and C from the Maps pool, v = TA review count). No mixed scales: per category uniformly rank OR the rating fallback; a single place without a rank is imputed like a missing TA source.57- **Blogs:** 0 mentions -> 0 · 1 -> 4 · 2 -> 7 · 3+ -> 10. **0 mentions is a regular score, not a "missing source".**58- **Only TA can be missing** (Maps is the entry gate, blog 0 counts as a score): impute the TA subscore conservatively with the 25th percentile of the pool's TA subscores. No renormalizing, no penalty.5960**Fairness step (this is what makes the thirds real):** per category convert every subscore into a 0-10 percentile rank over the candidate pool (ties = average rank, scaled linearly to 0-10; "pool" = the category's shortlist, while m/C come from the longlist). **Total = mean of the three percentile scores.** Reason: raw spreads are unequal (Maps clusters tightly after damping, blogs jump in steps); without the alignment the blog steps dominate the ranking and "one third each" would be a lie.6162The score decides, but not blindly: on a stark source contradiction (Maps loves it, blogs warn of a tourist trap) read the reviews and decide with a reason; the decision goes into the master file.6364## Review digest (for every list entry, mandatory)6566Token-friendly, no full scroll: (a) the Maps review summary / keyword chips on the place page, (b) skim the 3-5 most relevant reviews, (c) deliberately 1-2 of the most critical negative reviews (deal-breaker check: scam? hygiene? tourists-only?). For the **top 3 of every list** additionally cross-check the already collected TA/blog quotes.6768Result per place: **max 6 sentences**, compact and straight to the point, no filler. Pattern: what genuinely makes the place special (review essence), 1 concrete practical tip (what to order / when to go / book ahead), a warning if one exists, the closing-day constraint, phone. This text becomes the note 1:1.6970## Maps build (engine: google-maps 3.0, do not script it yourself)71721. **FIRST finish the master file completely** (ranked lists, final note texts, sources, anchors). It is the source of truth; the build afterwards is pure transfer.732. Generate `data.json` in the engine format from it (google-maps 3.0): per category `{"list": "<emoji> <place> · <category>", "emoji": ..., "places": [...]}`, **places in rank order (#1 first)**; the engine reverses on its own, does anchor validation, order auto-repair, notes after the final order, and the end verify.743. Note per place (identical in master file and data.json): prefix `🥇`/`🥈`/`🥉` for the top 3, `4 ·` to `10 ·` for the rest, then the review digest (max 6 sentences). The style convention in google-maps 3e applies hard (no markdown, no address duplication, no filler).754. **⭐ list** `⭐ <place> · Top Picks` as the extra entry in data.json: the top 3 of every category, notes here `🥇 Restaurants · <one-liner why>`. The user can then switch the category lists off and only ⭐ on: only the best on the map.765. Build: `python3 ~/.claude/skills/google-maps/scripts/maps_lists.py build data.json`. Exit 0 means verified. A crash rerun is safe (existence check and aria-checked idempotency are built in). Afterwards spot-read 2-3 notes.7778## Report7980- Per list the top 3 with a one-liner and score, below that briefly what narrowly missed and **why** (reasons are mandatory, never just the final list).81- Name the master file path. Share links optional (google-maps 3f; on dropdown flakiness hand it to the user as a manual step).8283## Failure playbook8485| Problem | Reaction |86|---|---|87| Crash mid-build | Rerun: the master file is the source of truth, the 3b/3d anchors prevent duplicates and toggle accidents |88| Session/token budget runs low | Save the master file state immediately, make a clean cut, build in a follow-up session (a rerun is cheap) |89| TripAdvisor blocks completely | Work the cascade; if it yields NOTHING, drop the TA dimension (total = mean of Maps and blog percentiles) and say so in the report |90| A selector breaks | Check google-maps section 6 pitfalls; the fix belongs in the engine `maps_lists.py` plus a section 6 row, no parallel script |91| UI structure unknown/changed | Do not guess: DOM probe (dump aria-labels/roles), add the finding to google-maps section 6 |