--- name: local-competitive-landscape description: Compare observed local business supply, categories, attributes, hours, and review-sample themes across defined areas using Crawlora's Google Maps dataset, live Google Maps, Apple Maps, and Yelp. Use for matched-area competitor landscapes and coverage comparisons, not lead generation, demand forecasts, sales estimates, or site-viability claims. --- # Local competitive landscape Compare like-for-like observations of businesses across defined areas. Produce a traceable market map without turning search-result coverage into a population, demand, revenue, or site-selection estimate. ## Setup and API contract Set `CRAWLORA_API_KEY` to your key from [crawlora.net](https://crawlora.net?utm_source=github&utm_medium=referral&utm_campaign=crawlora-skills). Run the bundled `scripts/crawlora.sh` from this skill directory or by absolute path. It sends `x-api-key` to `https://api.crawlora.net/api/v1`; keep the key in the environment. Read [reference/endpoints.md](reference/endpoints.md) for exact parameters and response behavior. Check the application `code` as well as HTTP status. Stop on `401`/`403`, back off on `429`, and retry a transient `5xx` once. A failed request is unavailable evidence, not an empty area. Keep Google Maps calls at least one second apart. ## Define a comparable frame 1. Record each named area and its operational boundary: administrative name, radius with center coordinates, or explicit polygon supplied by the user. Record the business concept, inclusion rules, closure treatment, sources, filters, retrieval date, page/limit budget, and whether all available pages or only a bounded sample will be collected. 2. Use identical category, geography type, status filters, dates, pagination, and source mix across areas. Do not treat a Google radius in meters as equivalent to an Apple viewport `span` in latitude/longitude degrees. Keep a separate coverage row for every source and area when geometry differs. 3. Discover exact dataset category values with `/datasets/google-map-businesses/facets?facet=category`; pass returned values unchanged. A text query can return businesses whose primary category differs, so preserve every source's returned category and apply the stated inclusion rule locally. ```sh scripts/crawlora.sh /datasets/google-map-businesses/facets \ facet=category q=coffee permanently_closed=false # Set CATEGORY to one exact `.data.items[].value` returned above. scripts/crawlora.sh /datasets/google-map-businesses/search \ category="$CATEGORY" lat=42.3736 lon=-71.1097 radius_m=2000 \ permanently_closed=false sort=distance_asc page=1 page_size=100 scripts/crawlora.sh -X POST /google/map/search \ '{"keyword":"coffee shop Cambridge Massachusetts","language":"en","country":"us"}' ``` ## Collect and reconcile coverage 1. Use `/datasets/google-map-businesses/search` for a bounded indexed-corpus baseline. Capture `total`, page, page size, filters, and whether the result window was exhausted. Dataset totals describe Crawlora's indexed corpus, not the business population. A zero or short result cannot prove local absence. Preserve each record's dataset update/crawl timestamp separately from request retrieval time; matched request dates do not make stored records equally fresh. Use `/datasets/google-map-businesses/items/{place_id}` only when a stored row needs its full record. 2. Run the same live Google query wording for every area. Post-filter returned coordinates or addresses against the defined boundary. Search ranking and a bounded result list can omit relevant businesses, so retain both returned and in-area counts. Refresh selected ambiguous or representative rows with `/google/map/place/{place_id}`. 3. Run Apple searches with the same query, regional catalog, language, span, and limit. Record Apple's returned region and `relocated`; discard results outside the intended boundary. A relocated search is evidence about Apple's fallback, not about the target area. Apple has no pagination, so a full returned viewport is still a bounded result set. 4. Run Yelp with the same term and location convention, paging only to the stated bound. Refresh selected rows with `/yelp/business/{id}`. Provider search areas and ranking differ; compare source-specific observations before merging them. 5. Preserve businesses with unknown coordinates or status in an unknown bucket. `permanently_closed=false` excludes confirmed closures but can retain dataset rows with `null` status; it does not certify they are open. Radius searches omit locationless service-area businesses, so run a matched text/area check if those businesses are in scope and disclose the omission otherwise. Deduplicate within each area and source by provider ID first. Across providers, require consistent name, address, coordinates, phone, or domain evidence. Keep chain branches separate unless the evidence identifies the same location. Retain source-specific IDs and disagreements after a merge. Do not assume a dataset `place_id` and live Google `place_id` are interchangeable when their formats or provenance differ: use the raw dataset ID for the stored-item endpoint, then resolve the business through live search using name and address before calling live Google detail unless ID compatibility has been established. Assign a business to every area whose boundary contains it, but do not sum overlapping-area counts as a unique total. ## Compare the areas Create three linked outputs: - **Coverage ledger:** area, source, query/category, geometry, filters, page or limit range, retrieved count, in-area count, unknown-location count, relocated status, exhausted/bounded/failed status, and retrieval time. - **Business matrix:** area membership, canonical branch, provider IDs, source names and categories, address/coordinates, status, hours/attributes when returned, rating, review count, source URLs, retrieval time, and match confidence. - **Comparison:** deduplicated observed counts and category/attribute mix within each matched frame, with unknowns and source disagreements shown beside every conclusion. Treat `rating: null` and `review_count: null` as unknown, distinct from numeric zero. Compare ratings only within the same platform and state sample sizes; do not blend Google, Apple, and Yelp rating scales or rank areas from cross-platform averages. Use `/google/map/place/{place_id}/reviews` only for a small, disclosed sample of visible review themes. Report theme counts within that sample, retain source/date, and do not infer customer-population prevalence. Missing hours or service attributes are unknown, never evidence that a service is absent. Keep returned attributes source-specific and date them; do not infer that an unlisted amenity, offering, or schedule is unavailable. Call a finding a **proven service gap** only when the service definition is explicit, complete eligible-business coverage establishes the candidate set, and verified service attributes establish absence across that set. Bounded map and directory listings ordinarily support only an **observed gap in the collected sample** with **search omission risk**. State which one the evidence supports. Do not turn business counts, ratings, or review themes into demand, revenue, market share, sales potential, or site-viability claims.