# Google Maps Service Opportunity Scorer (`zinin/google-maps-service-opportunity-scorer`) Actor

Prioritize Google Maps prospect research with an explainable business assessment. Score source-observed website, rating and listing gaps, separate unknown fields, and exclude closed locations.

- **URL**: https://apify.com/zinin/google-maps-service-opportunity-scorer.md
- **Developed by:** [Tim Zinin](https://apify.com/zinin) (community)
- **Categories:** Business, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.40 / 1,000 business assessments

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Maps Service Opportunity Scorer — turn an existing Maps prospect batch into an evidence-linked outreach queue

Feed it a Google Maps places batch you already scraped and get back one explainable business assessment per unique place — missing website, low rating with sufficient review volume, absent owner description, and sparse listing, each evidence-linked and weighted into an `opportunityScore` — never a revenue guess or a claim of independent verification.

![Google Maps Service Opportunity Scorer — hero diagram](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/d94b27ab62fe5f65d0258277507ea8d341761c8d/value-add-20260923/google-maps-service-opportunity-scorer/readme-hero.webp)

This Actor does not scrape Google Maps. It reads places already extracted by a Maps scraper — most naturally [Google Maps Scraper](https://apify.com/compass/crawler-google-places) or [Google Maps with Contact Details](https://apify.com/lukaskrivka/google-maps-with-contact-details) — via Dataset, run, or inline items, and scores each unique place against an explicit, user-adjustable service playbook. It is a research-prioritization tool for local marketing agencies working from a batch they already collected — it is not an audience-similarity score, a revenue predictor, or an email verifier.

### What you get

For each distinct place found across your source batch, you get one business assessment record containing:

- **`entityId`**, **`placeId`**, **`cid`** — the place's stable identity, bounded and type-validated.
- **`signals`** — a per-signal state for each of the four playbook checks: `missingWebsite`, `lowRating`, `missingOwnerDescription`, `sparseListing`, each independently resolved to `dataset_missing`, `observed_blank`, `observed_absent`, `present`, `healthy`, `observed_low`, `unproven_low_rating`, `invalid`, `invalid_type`, `sparse`, or `sufficient` — never a bare true/false that hides *why*.
- **`opportunityScore`**, **`opportunityScoreNormalized`**, **`weightTotal`** — the raw sum of triggered signal weights, that sum normalized against the total of all four configured weights, and the exact denominator used.
- **`status`** and **`action`** — `closed_excluded`, `high_opportunity`, `moderate_opportunity`, `low_opportunity`, `healthy_listing`, or `insufficient_data`, each paired with a concrete next step.
- **`includedInOutreachQueue`** — a convenience boolean mirroring `status`.
- **`evidence`** — up to 50 entries, each citing the literal source field, the exact rule that fired, and the source row it came from.
- **`confidence`** and **`partial`** — the fraction of the four signals that resolved, and a composite disclosure flag for any conflict, truncation, or unresolved signal.
- **Honest diagnostics for everything else** — a row with no `placeId`/`cid`, an oversized identity, or a place with no assessable field, are all free rows, never silently scored as "healthy."

What this Actor explicitly is **not**: no revenue estimate, no lead-conversion probability, no independent confirmation beyond what the source dataset recorded. A `dataset_missing` website field means the source row didn't carry that column — never proof the business itself lacks a website.

### Who uses it

- **Local marketing and SEO agencies** triaging a batch of Maps prospects by an explicit, adjustable playbook instead of eyeballing spreadsheets row by row.
- **Outreach and business-development teams** who need a defensible reason a place made the queue, rather than an opaque lead score.
- **Franchise and multi-location auditors** scoring their own portfolio for missing websites, thin listings, or low ratings with enough review volume to act on.
- **Data-quality teams** auditing an upstream Maps scrape, using the per-signal states as a signal for how clean the underlying data actually is.

Not a scraper: point it at an existing places Dataset from an upstream Actor, most naturally [Google Maps Scraper](https://apify.com/compass/crawler-google-places) (`users30d=37173`, `successfulRuns30d=3,751,440`) or [Google Maps with Contact Details](https://apify.com/lukaskrivka/google-maps-with-contact-details) (`users30d=3240`, `successfulRuns30d=72,898`). This Actor never starts either one, and never fetches a page from the open web.

### How to run

1. **Console (fastest for a one-off run).** Open the Actor, go to Input, paste rows into **Source records (inline)** or fill in **Source Dataset**, leave defaults, hit **Start**.
2. **From an existing Maps Dataset.** Put its Dataset ID in `datasetId` — read with your own run's permissions, no separate authorization step.
3. **Chained via Integrations.** Add this Actor as an "Actor/task run" integration on your upstream Maps-scraper Actor; map `{{resource.defaultDatasetId}}` into this Actor's `datasetId` field explicitly (see **Integration recipes**) for a `READ` grant — a bare Dataset ID or webhook payload alone only *requests* access, it never guarantees it.
4. **Via the API.** Call `POST /v2/acts/Mw6Fno8qkRJUzgylV/runs` (or `run-sync-get-dataset-items`) with `datasetId` (or `items`) and, for a spending ceiling, the run option `maxTotalChargeUsd`.
5. **Tune the playbook.** `minimumReviews`, `lowRatingThreshold`, and `weights` control what counts as an opportunity and how heavily each signal is weighted — defaults are 10 reviews, a 3.5 threshold, and 40/30/15/15 weights.
6. **Read the results.** One row per unique place lands in the run's default Dataset, with a matching `OUTPUT`/`SUMMARY` record describing delivery, billing, and coverage.

Every run is independent processing — see **Operating guide** for what that means for re-running against the same batch.

### Pricing

Pay-per-event, one event type: **`result-found`** ("Business assessment" in the platform's pricing UI — the primary pricing noun used throughout this listing, matching this product's own `noun` field) — one delivered business assessment (one unique place). There is **no start fee**, and every free diagnostic (a missing identity, an insufficient-data row, a budget stop, an empty batch) requests no charge event at all.

| Tier | Price per business assessment | Discount |
|---|---|---|
| Free (default) | $0.012 | — |
| Bronze | $0.0108 | 10% |
| Silver | $0.0096 | 20% |
| Gold | $0.0084 | 30% |
| Platinum | $0.0084 | 30% |
| Diamond | $0.0084 | 30% |

These are Apify's platform-wide account pricing tiers — not a volume schedule this Actor invents — assigned automatically by your account's usage; you don't select one. One business assessment is one unique place across the whole batch, never billed per duplicate source row.

**A note on the evidence in this README:** every output example below comes from the product owner's own verification runs against this Actor's accepted build — cost to the owner, not sales or customer revenue.

Set the platform's own **Max total charge (USD)** run option to cap spend. If testing the zero-paid path via a raw API call, do not rely on literal `maxTotalChargeUsd: 0` — the platform's own API-level cap-normalization behavior means a positive value below every tier's price only reliably guarantees zero paid results when the effective cap the run actually enforces stays at that value (inspect it after the run starts) — see **Evidence and boundaries** for the real recorded run demonstrating this.

### Input contract

| Field | Type | Default | Notes |
|---|---|---|---|
| `items` | array (JSON editor), max 1000 entries | prefilled real example | Inline source records. Ignored if `datasetId`, `runId`, or a resolvable `payload.resource` is also given. |
| `datasetId` | string (Dataset picker, READ) | — | Dataset ID from a Maps-scraper run. Read with your own run's permissions. Overrides inline `items`. |
| `runId` | string, max 64 chars | — | An Actor run ID to read instead of a Dataset ID; its default Dataset resolves under your own permissions — no upstream Actor started. |
| `payload` | object (JSON editor) | — | Optional `ACTOR.RUN.SUCCEEDED`-shaped payload carrying `resource.defaultDatasetId`/`resource.id`. The `datasetId` resource picker is the recommended way to request access; a bare payload reference alone does not grant it. |
| `maxInputRows` | integer | 100 | Hard cap on source records examined before grouping (1–1000). Still bounded to 4 MiB total / 64 KiB per row. |
| `maxResults` | integer | 10 | Max useful results this run can deliver (1–1000), independent of the platform's Max total charge option. |
| `minimumReviews` | integer | 10 | A low `totalScore` only counts when `reviewsCount` is at least this many; below it, the rating is `unproven_low_rating`, never scored either way. |
| `lowRatingThreshold` | number | 3.5 | `totalScore` strictly below this, plus `minimumReviews` met, triggers the low-rating signal. |
| `weights` | object (JSON editor) | `{missingWebsite:40, lowRating:30, missingOwnerDescription:15, sparseListing:15}` | Points added per triggered signal. Must not sum to zero across all four — an all-zero configuration is rejected, never silently defaulted. |

**Source precedence, exactly:** an explicit `datasetId` or `payload.resource.defaultDatasetId` (they must agree if both are present) beats a `runId` or `payload.resource.id`, which beats inline `items`. A dataset/run source is never merged with inline rows. A malformed or conflicting reference is rejected as invalid input, never silently downgraded to the inline example. Private-Dataset access is conditional: the resource picker or an authorized Integrations mapping can *request* a `READ` grant, but neither one *guarantees* the grant is actually honored by the platform — a bare Dataset ID string or webhook `resource.defaultDatasetId` reference alone never grants access to data you aren't already permitted to read.

**Accepted row fields** (any subset present is used; nothing is strictly required except a place identity):

| Field | Role |
|---|---|
| `placeId` / `cid` | Primary entity identity. `placeId` (string, ≤500 chars) is preferred; `cid` (string or finite number) is used when `placeId` is absent. Over the bound, the row is rejected outright (`oversized_identity`), never truncated. Neither present is a free `missing_identity` diagnostic. |
| `website` | `dataset_missing` (key absent — unknown), `observed_absent` (explicit blank — a real signal), `present` (non-blank string), or `invalid_type` (non-string, non-blank — never scored present). |
| `totalScore` / `reviewsCount` | Google's own rating (0–5) and review count (non-negative integer). Both must be present and well-typed to resolve; `unproven_low_rating` applies when the score is low but volume is below `minimumReviews`. |
| `ownerDescription` | Same four-state pattern as `website`. |
| `categoryName` / `categories` / `address` / `url` | Feed the composite sparse-listing check: only fields the source dataset actually carried (present or observed-absent) count toward "how many of the 3 core fields are present" — columns missing entirely contribute zero either way. |
| `permanentlyClosed` / `temporarilyClosed` | Either `true` excludes the place from outreach entirely, regardless of every other signal. |
| `title` | Pass-through display text, bounded to 500 chars and never assessment evidence on its own — a bare title with no other informative field is a free `insufficient_source_data` diagnostic. |
| `scrapedAt` | Picks the canonical occurrence when duplicate rows share the same identity — most recent valid timestamp wins, else the last occurrence deterministically. |
| `found`, `failed`, `status`, `error`, `errors` | An explicit hard-failure signal (`found: false`, `failed: true`, or a known failure-alias `status`) is always a free `source_reported_failure` diagnostic, even with `found: true`/`partial: true` also present. A non-empty `error`/`errors` alone is a harmless warning ONLY when the same row also explicitly carries `found: true` **and** `partial: true`; otherwise it still produces the free diagnostic. |

**Hard read bounds, always enforced regardless of `maxInputRows`:** at most 1000 source rows, at most 4 MiB of total source JSON per run, at most 64 KiB per individual row (an oversized row is dropped and disclosed). This Actor reads only the fixed `https://api.apify.com` origin with your run's own token — it never fetches an arbitrary URL and never starts an upstream Actor.

### Output examples

Every example below is a real record from an actual accepted run of this Actor's build `0.1.3` (Actor ID `Mw6Fno8qkRJUzgylV`, build `e0w6gOHVNRhY37xqS`), reproduced exactly — nothing is hand-edited. Source dates come from `scrapedAt` (2026-09-22T23:07:38–42Z); the runs were processed 2026-09-23 — a saved snapshot, not a live re-check.

#### 1. First useful assessment — a full, real dataset row

This run used the Actor's own real inline prefill: 3 real London museum listings. Both diagrams on this page illustrate this exact run — the same 3 real places, all delivered as paid results.

Actor input (first of three real prefill rows; abbreviated to the fields this Actor reads):

```json
{
  "title": "Natural History Museum",
  "ownerDescription": null,
  "categoryName": "Natural history museum",
  "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
  "website": "https://www.nhm.ac.uk/",
  "totalScore": 4.6,
  "reviewsCount": 39058,
  "permanentlyClosed": false,
  "temporarilyClosed": false,
  "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "cid": "8914844151054820548",
  "url": "https://www.google.com/maps/search/?api=1&query=Natural%20History%20Museum&query_place_id=ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "scrapedAt": "2026-09-22T23:07:38.397Z",
  "_exampleProvenance": {
    "sourceActorId": "nwua9Gu5YrADL7ZDj",
    "sourceRunId": "5VeVFvxu0MQhPBrWY",
    "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
    "curatedPublicSnapshot": true,
    "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
  }
}
```

First Dataset output row (of three delivered from this run):

```json
{
  "entityId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "found": true,
  "status": "low_opportunity",
  "action": "add_to_outreach_queue_low_priority",
  "confidence": 1,
  "sourceRowCount": 1,
  "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "cid": "8914844151054820548",
  "title": "Natural History Museum",
  "titleTruncated": false,
  "opportunityScore": 15,
  "opportunityScoreNormalized": 15,
  "weightTotal": 100,
  "signals": {
    "missingWebsite": "present",
    "lowRating": "healthy",
    "missingOwnerDescription": "observed_absent",
    "sparseListing": "sufficient"
  },
  "includedInOutreachQueue": true,
  "observedAt": "2026-09-22T23:07:38.397Z",
  "sourceObservationWindow": { "earliest": "2026-09-22T23:07:38.397Z", "latest": "2026-09-22T23:07:38.397Z", "validCount": 1, "totalRowCount": 1 },
  "evidence": [
    { "field": "website", "value": "present", "rule": "present: source recorded a non-blank website value in this row", "sourceRowIndex": 0 },
    { "field": "totalScore", "value": 4.6, "rule": "healthy_rating: source-observed totalScore 4.6 and reviewsCount 39058 do not meet the low-rating threshold (3.5)", "sourceRowIndex": 0 },
    { "field": "ownerDescription", "value": null, "rule": "observed_absent: source-observed empty owner description in this row", "sourceRowIndex": 0 },
    { "field": "categoryName/address/url", "value": 3, "rule": "sufficient_listing: source-observed listing has 3 of 3 core fields (category, address, url) present in this row", "sourceRowIndex": 0 }
  ],
  "businessPartial": false,
  "sourceRowIndexes": [0],
  "partial": false,
  "sourceCoverage": { "complete": true, "reason": null, "droppedOversized": 0 },
  "product": "google-maps-service-opportunity-scorer",
  "schemaVersion": "1.0",
  "sourceDatasetId": null,
  "sourceRunId": null,
  "suppliedSourceRunId": null,
  "sourceRunIdVerified": null,
  "processedAt": "2026-09-23T05:37:55.035Z"
}
```

`observedAt`/`sourceObservationWindow` reflect the sole contributing occurrence's real, whitespace-trimmed `scrapedAt` — `earliest`/`latest` are identical here because only one row contributed.

`opportunityScore: 15` reflects only the `missingOwnerDescription` signal triggering (weight 15 of the default 40/30/15/15 playbook) — the website is present and the rating is healthy, so this museum is a low-priority outreach candidate, not a strong lead. `opportunityScoreNormalized: 15` equals the raw score here because the default weights already sum to 100.

#### 2. Derived partial-flag control — same real values, an explicit incompleteness flag injected

This is **not** a demonstration of removed columns or actual upstream data incompleteness. It is the identical real discovery batch as example 1, with each source row's own `partial` field deliberately set to `true` (and `found: true` added), to exercise this Actor's disclosed-incompleteness path under a controlled, known input. Every underlying value is identical and real; only the incompleteness flag is a deliberate test control, not an observed condition from any actual scrape.

Actor input (identical to example 1, except `"found": true, "partial": true` added):

```json
{
  "title": "Natural History Museum",
  "ownerDescription": null,
  "categoryName": "Natural history museum",
  "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
  "website": "https://www.nhm.ac.uk/",
  "totalScore": 4.6,
  "reviewsCount": 39058,
  "permanentlyClosed": false,
  "temporarilyClosed": false,
  "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "cid": "8914844151054820548",
  "url": "https://www.google.com/maps/search/?api=1&query=Natural%20History%20Museum&query_place_id=ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "scrapedAt": "2026-09-22T23:07:38.397Z",
  "_exampleProvenance": {
    "sourceActorId": "nwua9Gu5YrADL7ZDj",
    "sourceRunId": "5VeVFvxu0MQhPBrWY",
    "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
    "curatedPublicSnapshot": true,
    "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
  },
  "found": true,
  "partial": true
}
```

Resulting row (`businessPartial: true`, `partial: true` — otherwise identical to example 1's row):

```json
{
  "entityId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "found": true,
  "status": "low_opportunity",
  "action": "add_to_outreach_queue_low_priority",
  "confidence": 1,
  "sourceRowCount": 1,
  "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "cid": "8914844151054820548",
  "title": "Natural History Museum",
  "titleTruncated": false,
  "opportunityScore": 15,
  "opportunityScoreNormalized": 15,
  "weightTotal": 100,
  "signals": {
    "missingWebsite": "present",
    "lowRating": "healthy",
    "missingOwnerDescription": "observed_absent",
    "sparseListing": "sufficient"
  },
  "includedInOutreachQueue": true,
  "observedAt": "2026-09-22T23:07:38.397Z",
  "sourceObservationWindow": { "earliest": "2026-09-22T23:07:38.397Z", "latest": "2026-09-22T23:07:38.397Z", "validCount": 1, "totalRowCount": 1 },
  "evidence": [
    { "field": "website", "value": "present", "rule": "present: source recorded a non-blank website value in this row", "sourceRowIndex": 0 },
    { "field": "totalScore", "value": 4.6, "rule": "healthy_rating: source-observed totalScore 4.6 and reviewsCount 39058 do not meet the low-rating threshold (3.5)", "sourceRowIndex": 0 },
    { "field": "ownerDescription", "value": null, "rule": "observed_absent: source-observed empty owner description in this row", "sourceRowIndex": 0 },
    { "field": "categoryName/address/url", "value": 3, "rule": "sufficient_listing: source-observed listing has 3 of 3 core fields (category, address, url) present in this row", "sourceRowIndex": 0 }
  ],
  "businessPartial": true,
  "sourceRowIndexes": [0],
  "partial": true,
  "sourceCoverage": { "complete": true, "reason": null, "droppedOversized": 0 },
  "product": "google-maps-service-opportunity-scorer",
  "schemaVersion": "1.0",
  "sourceDatasetId": null,
  "sourceRunId": null,
  "suppliedSourceRunId": null,
  "sourceRunIdVerified": null,
  "processedAt": "2026-09-23T05:38:09.129Z"
}
```

Notice the assessment itself is unchanged from example 1; only `businessPartial`/`partial` flip to `true` because the source row disclosed its own incompleteness — exactly what a genuinely partial real scrape would produce. This "partial-columns" fixture is a historical folder-name label only, sourced from this actor's own adversarial-provenance record: the actual test retains every real value and injects `found:true`/`partial:true` as a controlled flag, never removing or blanking a real column.

#### 3. Below-price run — free `budget_stopped` diagnostic, no partial charge

```json
[
  {
    "product": "google-maps-service-opportunity-scorer",
    "schemaVersion": "1.0",
    "status": "budget_stopped",
    "reason": "Run charge limit cannot cover any result; no source was read.",
    "found": false,
    "processedAt": "2026-09-23T05:38:20.929Z"
  }
]
```

This run used the platform run option `maxTotalChargeUsd: 0.005` — below every tier's per-assessment price (even Gold/Platinum/Diamond's $0.0084). The Actor never reads the source batch, delivers zero paid assessments, and writes this one free diagnostic. See **Evidence and boundaries** for the API-level zero-cap caveat.

#### 4. Empty batch — free `no_results` diagnostic

Actor input:

```json
{ "items": [], "maxInputRows": 100, "maxResults": 10 }
```

Dataset output:

```json
[
  {
    "product": "google-maps-service-opportunity-scorer",
    "schemaVersion": "1.0",
    "status": "no_results",
    "found": false,
    "reason": "no_usable_rows_in_batch",
    "sourceCoverage": { "complete": true, "reason": null, "droppedOversized": 0 },
    "processedAt": "2026-09-23T05:38:30.802Z"
  }
]
```

An empty or entirely-unusable batch is a free diagnostic, never a silent zero-row success and never a charge.

### Field dictionary

**Per-assessment result fields:**

| Field | Type | Meaning |
|---|---|---|
| `entityId` | string | `placeId`, or `cid:<cid>` when `placeId` is absent — stable key for this place. |
| `found` | boolean | `true` on every billed assessment; `false` on every free diagnostic. |
| `status` | string | `closed_excluded`, `high_opportunity`, `moderate_opportunity`, `low_opportunity`, `healthy_listing`, or `insufficient_data` on an assessment; a diagnostic reason (`invalid_row`, `source_reported_failure`, `missing_identity`, `oversized_identity`, `insufficient_source_data`, `invalid_domain_options`, `budget_stopped`, `no_results`, `max_results_reached`) on a free row. |
| `action` | string | `skip_outreach_closed`, `add_to_outreach_queue_priority`, `add_to_outreach_queue`, `add_to_outreach_queue_low_priority`, `manual_verification_needed`, or `no_action_needed`. |
| `placeId` / `cid` | string/number, null | Bounded, type-validated identity fields; a malformed non-primary identity is nulled rather than echoed verbatim. |
| `title` / `titleTruncated` | string/null, boolean | Pass-through text bounded to 500 chars; `true` when truncated (a wrong-typed title is nulled instead, disclosed via evidence and `partial: true`). |
| `opportunityScore` | number | Raw sum of triggered signal weights — not itself compared against the fixed thresholds. |
| `opportunityScoreNormalized` | number | `100 * opportunityScore / weightTotal` — compared against `high_opportunity` (≥50) / `moderate_opportunity` (≥20). |
| `weightTotal` | number | Sum of ALL four configured weights, including unresolved signals — so an unresolved signal's weight can't inflate the percentage attributed to signals that did resolve. |
| `signals` | object | `{ missingWebsite, lowRating, missingOwnerDescription, sparseListing }`, each one of `dataset_missing`, `observed_blank`, `observed_absent`, `present`, `healthy`, `observed_low`, `unproven_low_rating`, `invalid`, `invalid_type`, `sparse`, `sufficient`. |
| `includedInOutreachQueue` | boolean | Convenience flag mirroring `status`. |
| `confidence` | number | Fraction (0–1) of the four signals that resolved to an assessable value. |
| `evidence` | array | Up to 50 entries, each `{ field, value, rule, sourceRowIndex }`. |
| `sourceRowCount` / `sourceRowIndexes` | number / array | True count and (capped at 50) list of contributing rows, including failed duplicates sharing this identity. |
| `businessPartial` / `partial` | boolean | `businessPartial` is this module's own signal; `partial` also folds in run-level source incompleteness — check `partial` if you only check one. |
| `sourceCoverage` | object | Run-level: `{ complete, reason, droppedOversized }`. |
| `sourceDatasetId`, `sourceRunId`, `suppliedSourceRunId`, `sourceRunIdVerified` | string/null, boolean/null | Where the batch came from; `sourceRunId` set only when independently verified. |
| `processedAt` | string (ISO 8601) | When processed — not when scraped (see `scrapedAt` in your own rows). |
| `observedAt` | string (ISO 8601), null | The latest strictly calendar-valid `scrapedAt` across **every** accepted occurrence contributing to this entity (not only the canonical row), computed before any display/evidence cap. The exact captured text, whitespace-trimmed only — never re-serialized, never re-formatted, never a processing-time/"now" fallback. `null` when no contributing occurrence carried a valid `scrapedAt`. |
| `sourceObservationWindow` | object | `{ earliest, latest, validCount, totalRowCount }` — `earliest`/`latest` are the raw `scrapedAt` strings as originally reported by the earliest/latest-valid occurrence, whitespace-trimmed only (never re-serialized/re-formatted); `validCount` is how many contributing occurrences had a strictly valid `scrapedAt`; `totalRowCount` is how many accepted occurrences contributed to this entity at all — `validCount` is always ≤ `totalRowCount`, since an occurrence with an invalid, missing, or date-only `scrapedAt` still counts toward `totalRowCount` but never toward `validCount`. |
| `reason` / `error` | string | Present on free diagnostic rows only. |

**Complete runtime OUTPUT/SUMMARY field table** (the Key-value store record under both the `OUTPUT` and `SUMMARY` keys — identical payload, two conventional key names; no `metadata` field exists in this runtime's real output):

| Field | Meaning |
|---|---|
| `schemaVersion` | This product's output schema version string (`"1.0"`). |
| `product` | `"google-maps-service-opportunity-scorer"`. |
| `outcome` | `delivered`, `budget_stopped`, `max_results_reached`, `no_results`, `diagnostic`, or `failed`. |
| `errorCode` | Short internal code on a non-nominal outcome, or `null`. |
| `fatalFailure` | `true` only when `outcome === "failed"`. |
| `deliveryUncertain` | A Dataset-write or charge acknowledgment is uncertain, including FREE\_WRITE\_FAILED; raw rows can exceed confirmed counters. |
| `replayBlocked` | `true` on either of two distinct refusal causes: the run's own `result-found` event counter didn't match its expected record (`EXISTING_RESULT`), or this run's claim key was already set from a prior attempt and automatic resurrection is unsupported (`EXISTING_CLAIM`). |
| `requested` | Source rows actually read this run. |
| `usefulResults` | Results found after oversized-row filtering, before budget/`maxResults` cap. |
| `delivered` | Paid, confirmed assessments actually written. |
| `paid` | Confirmed `result-found` charge events, cross-checked against `delivered`. |
| `free` | Free diagnostic rows actually written — see **Evidence and boundaries** for how this can exceed the 200-row business-diagnostic cap. |
| `failed` | Useful results dropped for exceeding the 64 KiB per-row limit (rare). |
| `sourcePartial` / `sourcePartialReason` | Whether the source READ was incomplete, and why. |
| `sourceDroppedOversized` | Source rows dropped for exceeding 64 KiB before reaching assessment logic. |
| `budgetStopped` | `true` if the charge limit was reached before all useful assessments were delivered. |
| `resultsCapped` | `true` if `maxResults` was reached before all useful assessments were delivered. |
| `businessPartialResults` | Partial transform results counted before size, budget and result-cap filtering; not a delivered-result count. |
| `oversizedDiagnosticsDropped` | Diagnostic rows dropped for exceeding 64 KiB (rare). |
| `diagnosticsTruncated` | `true` if more diagnostics existed than the runtime's write cap could hold — see **Evidence and boundaries** for what this counts. |
| `partialOverall` | `true` if any of the above fired — check this one field if you only check one. |
| `budgetMode` | `unlimited`, `finite`, or `local_no_charging`. |
| `maxTotalChargeUsd` | The effective run charge cap enforced, or `null` when unlimited/local. |
| `replaySafety` | Fixed disclosure string: re-running is new processing, no cross-run dedup. |
| `businessSummary` | `{ totalSourceRows, resultsCount, diagnosticsCount, statusBreakdown, observedAt }` — `diagnosticsCount` counts only this product's own business diagnostics, never the run-level notices layered on top (see **Evidence and boundaries**). |
| `processedAt` | ISO 8601 timestamp of this OUTPUT/SUMMARY write. |

### Evidence and boundaries

**Hard failure always wins, even against a contradictory `found:true, partial:true` claim.** A row with `found: false`, `failed: true`, or a `status` matching a known failure alias (`failed`/`error`/`timed-out`/`timedout`/`timeout`/`aborted`, trim+lowercase-normalized) is always a free `source_reported_failure` diagnostic — a source cannot simultaneously claim "this row failed" and "this row is a usable partial result." A non-empty `error`/`errors` field with no hard-failure signal present is retained as a harmless warning ONLY when the same row explicitly carries both `found: true` and `partial: true`; in every other case it still produces the free `source_reported_failure` diagnostic.

**Four-field-state discipline, never conflated.** `website`, `ownerDescription`, and the sparse-listing component fields each resolve independently to `dataset_missing` (key genuinely absent — unknown, never a claim the business lacks it), `observed_blank`/`observed_absent` (key present but explicitly blank — a real, source-observed gap), `invalid_type` (key present but wrong type — ambiguous, never counted either way), or `present`. Only fields the source dataset actually carried can count toward a signal triggering; missing columns contribute zero points.

**A low rating backed by too few reviews is `unproven_low_rating`, never `healthy`.** A 2.1/5 score with 3 reviews contributes zero low-rating points (too few reviews to confidently act on) but is never scored or reported as `healthy` either, and always marks the result `partial`.

**A bare title is never assessment evidence on its own.** A row where every signal resolves to `dataset_missing`/`observed_blank`/`invalid`/`invalid_type`/`unproven_low_rating` is a free `insufficient_source_data` diagnostic, never a paid "healthy" or "sparse" claim.

**A closed business is excluded from outreach outright, before any signal is even scored.** `permanentlyClosed: true` or `temporarilyClosed: true` produces `status: closed_excluded`, `opportunityScore: 0`, and an empty `signals` object.

**Duplicate identities are resolved deterministically, and a failed duplicate is disclosed, never scored.** When multiple rows share the same `placeId`/`cid`, the row with the most recent valid `scrapedAt` is canonical (else the last occurrence). A duplicate that itself reported a hard failure is excluded from evidence, but disclosed via `sourceRowCount` and `partial: true` on the surviving result. Canonical selection is a separate mechanism from `observedAt`/`sourceObservationWindow`: canonical selection decides which one occurrence's display fields (title, address, etc.) are used; the observation window spans `scrapedAt` across **every** accepted occurrence sharing the identity, whether or not that occurrence is the canonical one.

**Bounded, disclosed caps, applied only to display, never to the underlying determination:** `evidence` capped at 50 entries; `sourceRowIndexes` capped at 50 (`sourceRowCount` is the true total); `title`/pass-through free-text fields bounded to 500 characters (truncated, never silently merged with another value); a malformed non-string value in a text field is nulled, never echoed verbatim; a blank string over a few characters is described by length in evidence (`<blank string, length 65000>`) rather than copied raw.

**Business diagnostics are capped at 200 written rows — but total free rows for a run can legitimately exceed that, and `businessSummary.diagnosticsCount` counts business-only.** The initial diagnostics array the shared runtime writes from consists of this product's own `report.diagnostics` PLUS the runtime's own `partial_source`/`source_rows_dropped` notices, combined and then sliced to 200 written rows; `diagnosticsTruncated` covers that COMBINED list, while `businessSummary.diagnosticsCount` counts only this product's own business diagnostics, never the runtime notices layered on top. Separately, later notices — `result_dropped_oversized`, `budget_stopped`, or `max_results_reached` — are written on top of that 200-row cap and are not counted against it, raising the total free-row count above 200. Verified against a real 1,000-row batch (999 synthetic `source_reported_failure` rows plus a real 3-place batch tail, `maxResults: 1`): exactly 200 business diagnostics were written, one further `max_results_reached` notice was added, for **201 total free rows** (`freeRows: 201`) — never a blanket "200 free cap." `businessSummary.diagnosticsCount` in that same run reported `997` (the true diagnostic count before the 200-row write slice), not the count of rows actually written.

**Budget behavior — read this before relying on a specific `maxTotalChargeUsd` value via the API.** A positive `maxTotalChargeUsd` below the price of one assessment correctly delivers zero paid assessments and one free `budget_stopped` diagnostic (output example 3, $0.005 against a $0.0084 floor tier). Separately, the platform's own API-level cap-normalization behavior applies to a raw `maxTotalChargeUsd: 0` value. **A positive value below every tier's price only reliably guarantees zero paid results while the effective cap is preserved at that value — inspect the run's actual, effective `options.maxTotalChargeUsd` after it starts** rather than assuming the requested value silently held.

**Delivery is not transactional, and a `FAILED` run does not mean "nothing happened."** Each assessment is written to the Dataset and then charged as two separate steps; a run failing between them can leave an effectively free, unconfirmed write with no automatic retry — no cross-run exactly-once guarantee. If pricing/charge-limit/event-counter state can't be verified mid-run, the run finalizes `FAILED` with a safe explanation. **Inspect the original run's Dataset and `OUTPUT`/`SUMMARY` record before a manual retry** — a blind retry after an ambiguous outcome can duplicate paid assessments.

**Field values are read from the actual output, never assumed from the schema description alone.** Every field, state, and count above was independently confirmed against this actor's own real evidence receipts — `businessSummary.diagnosticsCount` genuinely differs from `free` in the mixed-boundary receipt (`997` vs `201`), which only reading the real `summary.json` makes clear.

### Decision routing

- **`status: high_opportunity`, `action: add_to_outreach_queue_priority`** — `opportunityScoreNormalized` ≥ 50. A strong outreach candidate.
- **`status: moderate_opportunity`, `action: add_to_outreach_queue`** — `opportunityScoreNormalized` ≥ 20 and < 50.
- **`status: low_opportunity`, `action: add_to_outreach_queue_low_priority`** — `opportunityScoreNormalized` > 0 but below 20, as in output example 1.
- **`status: healthy_listing`, `action: no_action_needed`** — every resolved signal came back healthy/present/sufficient.
- **`status: insufficient_data`, `action: manual_verification_needed`** — score is 0 but at least one signal never resolved — distinct from a genuinely healthy listing.
- **`status: closed_excluded`, `action: skip_outreach_closed`** — the source marked this place closed; never scored, never queued.
- **`businessPartial: true` / `partial: true`** — disclosed incompleteness, a duplicate-row conflict, a truncated/invalid title, or an unresolved signal — worth a second glance.
- **Free diagnostic rows (`found: false`)** — never an assessment. `missing_identity`/`oversized_identity` mean no stable identity; `insufficient_source_data` means no signal resolved; `invalid_domain_options` means your playbook input was rejected; `budget_stopped`/`max_results_reached` mean a run-level cap was hit.

### Commercial playbooks

**Tiered outreach triage.** Sort by `opportunityScoreNormalized` descending and work `high_opportunity` first — each row cites the exact evidence to open the conversation with.

**Custom playbook per client vertical.** Adjust `weights` per agency client — a restaurant client might weight `lowRating` heavier than `missingWebsite`. `weightTotal` keeps the scale comparable across configurations.

**Portfolio health audits.** Run your own franchise dataset through with default weights to surface listing gaps across locations, independent of any outreach intent.

**Data-quality triage.** A high proportion of `invalid_type`/`dataset_missing` signals signals your upstream Maps scraper is inconsistently capturing fields.

### Integration recipes

![Google Maps Service Opportunity Scorer — workflow diagram](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/d94b27ab62fe5f65d0258277507ea8d341761c8d/value-add-20260923/google-maps-service-opportunity-scorer/readme-workflow.webp)

This Actor's ID is **`Mw6Fno8qkRJUzgylV`**. A natural upstream source is [Google Maps Scraper](https://apify.com/compass/crawler-google-places) (Actor ID `nwua9Gu5YrADL7ZDj`); [Google Maps with Contact Details](https://apify.com/lukaskrivka/google-maps-with-contact-details) works identically. The recipes below describe how to configure each mechanism — not a claim any specific trigger is currently installed. A saved public example is available in the Tasks tab.

**1. Console-native chaining (no code).** On your Maps-scraper's run page, open **Integrations**, add an "Actor/task run" action targeting this Actor, and let the Integrations UI populate the payload with `{{resource.defaultDatasetId}}`. Map that value explicitly into this Actor's `datasetId` field — not the generic `payload` pass-through alone — to *request* a `READ` grant. The mapping requests the grant; it does not itself guarantee the platform honors it.

**2. Direct webhook (`POST /v2/webhooks`), full recipe.** Configure a webhook on your upstream Maps-scraper for the `ACTOR.RUN.SUCCEEDED` event, scoped to that specific Actor via its `condition.actorId`, targeting this Actor's run-creation endpoint:

```json
{
  "isApifyIntegration": true,
  "shouldInterpolateStrings": true,
  "eventTypes": ["ACTOR.RUN.SUCCEEDED"],
  "condition": { "actorId": "nwua9Gu5YrADL7ZDj" },
  "requestUrl": "https://api.apify.com/v2/acts/Mw6Fno8qkRJUzgylV/runs?maxTotalChargeUsd=0.50",
  "doNotRetry": true,
  "headersTemplate": "{\"Authorization\": \"Bearer {{APIFY_TOKEN}}\"}",
  "payloadTemplate": "{\"datasetId\": \"{{resource.defaultDatasetId}}\", \"payload\": {\"resource\": {\"defaultDatasetId\": \"{{resource.defaultDatasetId}}\", \"id\": \"{{resource.id}}\"}}, \"minimumReviews\": 5}"
}
```

Submit this to `POST https://api.apify.com/v2/webhooks` with your own `Authorization: Bearer <APIFY_TOKEN>` header. The `{{APIFY_TOKEN}}` placeholder is **not** an implicit platform template variable resolved automatically at trigger time — it must be provisioned separately: filled in from your own secrets store at the moment you register the webhook. `doNotRetry: true` avoids a duplicate-charge risk from an automatic retry. `datasetId` is set explicitly — that field is what actually requests access; `payload.resource` alone is informational provenance only. A positive `maxTotalChargeUsd` on the request URL caps spend; avoid literal `0` (see **Evidence and boundaries**).

**3. Generic automation tools (n8n, Zapier, Make, or your own tooling).** Use a generic Apify/HTTP node to call `run-sync-get-dataset-items` on this Actor's ID with a JSON body containing `datasetId` and your chosen `weights`/`minimumReviews`/`maxResults` — a plain authenticated HTTPS POST, no native integration required.

**4. Export / CRM-adjacent mapping.** Every run's Dataset supports CSV/Excel/JSON export via the Console or `GET /v2/datasets/{datasetId}/items?format=csv`. `entityId`, `title`, `status`, `action`, `opportunityScoreNormalized`, and `signals` alone drive a working outreach queue.

### Operating guide

1. **Confirm your source batch shape first.** Spot-check rows against **Input contract** — each needs `placeId` or `cid`, or you'll mostly get `missing_identity` diagnostics.
2. **Tune the playbook before a large run.** `minimumReviews`, `lowRatingThreshold`, and `weights` change what counts as an opportunity — test on a small `maxResults` first if you've changed the defaults.
3. **Start with a small `maxResults` on a new source** to confirm output matches expectations before a full batch.
4. **Set `maxTotalChargeUsd` via the platform run option, and check its effective value after the run starts.** For a guaranteed-zero-paid API call, use a small positive value below $0.0084, not literal `0` — see **Evidence and boundaries**.
5. **Read the `OUTPUT`/`SUMMARY` record, not just the Dataset row count** — it reports `delivered`/`paid`/`free`/`failed`, whether `budgetStopped`/`resultsCapped` ended the run early, and `diagnosticsTruncated`. Remember `businessSummary.diagnosticsCount` counts only this product's own diagnostics, not run-level notices layered on top.
6. **If a run finalizes `FAILED`, inspect its Dataset and `OUTPUT` record before retrying** — a blind retry can duplicate already-successful paid assessments.
7. **Re-running is always new billing** — no cross-run dedup key. Reserve it for a new or updated batch, not a routine safety retry.

### FAQ

**Does this Actor scrape Google Maps itself?** No. It only reads an existing places batch you or an upstream Actor already collected — no open-web fetch, no upstream Actor started.

**Can I use a source other than Google Maps Scraper?** Yes — any source producing rows with `placeId`/`cid` and the fields in **Input contract** works, including Google Maps with Contact Details.

**Does a high `opportunityScore` mean the business will convert?** No. It reflects how many playbook signals triggered and how heavily they're weighted — a research-prioritization score, not a probability of commercial success, revenue, or lead conversion.

**Why is a low rating sometimes not scored as an opportunity?** When `reviewsCount` is below `minimumReviews`, the low rating is `unproven_low_rating` — too few reviews to trust, but never counted as `healthy` either.

**Does `dataset_missing` mean the business doesn't have a website?** No. It means the source row's `website` column was absent entirely — genuinely unknown. Only an explicit blank value (`observed_absent`) is a real signal.

**Does re-running the same batch avoid double-billing?** No. Every run is independent with no cross-run deduplication.

**Is the "partial-columns" style example a demonstration of missing upstream data?** No — see output example 2. Same real values as the healthy example; only `partial`/`found` are deliberately set as a controlled test.

**Why did one run report 201 free rows against a "200 diagnostics" cap, and diagnosticsCount showed 997?** A separate run-level notice (`max_results_reached`) is written on top of the 200-row cap, not counted against it. `businessSummary.diagnosticsCount` reported the true 997 count before that 200-row write slice — see **Evidence and boundaries**.

### Sources and rights

This Actor reads only Datasets and runs your own Apify account token already has permission to read; it never uses another user's credentials and never starts an upstream Actor on your behalf. The public prefilled example is a curated, non-sensitive real snapshot of three London museum listings from a Google Maps scrape (`sourceActorId nwua9Gu5YrADL7ZDj`, `sourceRunId 5VeVFvxu0MQhPBrWY`, `sourceDatasetId WJkSy7pXIRxQlwPvn`), observed 2026-09-22, reused with its original values unchanged — a saved example, not a live re-check or a new fresh scrape. All output values here are copied verbatim from real runs of this Actor's accepted build; none are invented. These verification runs were the product owner's own cost, not customer sales or revenue evidence. This README does not claim to be currently public on the Apify Store, and does not claim the actor's current cloud build already serves it.

#### Related tools

Related tools for adjacent workflows in B2B lead generation and data enrichment.

| Actor | What it does |
|---|---|
| [Website Contact Scraper — Public Emails & Phones](https://apify.com/zinin/website-contact-extractor) | Pair it in the B2B lead generation and data enrichment workflow: Find public company-site emails, phones, and social profiles with page-level evidence, best-contact... |
| [Company Lookup — Website & GLEIF Evidence](https://apify.com/zinin/company-lookup) | Pair it in the B2B lead generation and data enrichment workflow: Turn domains, company names, or exact LEIs into evidence-linked website and GLEIF observations with... |
| [Lead List Qualifier - Contact, Hiring & Tech Signals](https://apify.com/zinin/lead-list-qualifier) | Pair it in the B2B lead generation and data enrichment workflow: Prioritize company domains with transparent contact, hiring, technology, and SEC-name signals |

This actor's own `crosslinks.json` entry lists exactly 3 related links, not 5, and the 3 real, verified links above are used as-is rather than padded to a higher count.

# Actor input Schema

## `items` (type: `array`):

Small real public example: https://api.apify.com/v2/datasets/D7IDV0iPRUoMflfx3/items?clean=true . This saved snapshot is not a new scrape.

## `datasetId` (type: `string`):

Read an existing source dataset with your run permissions. Overrides inline items. The resource picker can request scoped READ permission. Integration mapping must use an authorized resource; a plain Dataset ID or webhook payload alone does not grant access. Public example: D7IDV0iPRUoMflfx3.

## `runId` (type: `string`):

Optional Actor run ID to read instead of Source Dataset. Its default Dataset is resolved with your run's own permissions — no elevated access is used, and no upstream Actor is started.

## `maxInputRows` (type: `integer`):

Maximum source records examined before grouping. At most 1000 rows, 4 MiB total and 64 KiB per row. Capped or incomplete source coverage is reported.

## `maxResults` (type: `integer`):

Maximum useful business assessment results delivered, independently of the platform Max total charge setting. Free diagnostics are reported separately.

## `payload` (type: `object`):

Optional ACTOR.RUN.SUCCEEDED event payload with resource.defaultDatasetId and resource.id. Dataset resourcePicker is recommended for private datasets; a payload alone does not grant access.

## `minimumReviews` (type: `integer`):

A low totalScore only counts as an opportunity signal when reviewsCount is at least this many. Below the threshold, the rating is treated as unproven (too few reviews to be meaningful) and not scored.

## `lowRatingThreshold` (type: `number`):

totalScore strictly below this value, combined with minimumReviews being met, triggers the low-rating opportunity signal.

## `weights` (type: `object`):

Points added to opportunityScore per triggered signal. Adjust to match your own agency's outreach priorities. Signals with dataset\_missing data never contribute points regardless of weight.

## Actor input object example

```json
{
  "items": [
    {
      "title": "Natural History Museum",
      "subTitle": null,
      "description": "Landmark museum of animals and natural phenomena, with hands-on exhibits and animatronic dinosaurs.",
      "ownerDescription": null,
      "price": null,
      "categoryName": "Natural history museum",
      "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
      "neighborhood": "South Kensington",
      "street": "Cromwell Rd",
      "city": "London",
      "postalCode": "SW7 5BD",
      "state": null,
      "countryCode": "GB",
      "website": "https://www.nhm.ac.uk/",
      "phone": "+44 20 7942 5000",
      "phoneUnformatted": "+442079425000",
      "location": {
        "lat": 51.496715,
        "lng": -0.1763672
      },
      "totalScore": 4.6,
      "reviewsCount": 39058,
      "permanentlyClosed": false,
      "temporarilyClosed": false,
      "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
      "cid": "8914844151054820548",
      "url": "https://www.google.com/maps/search/?api=1&query=Natural%20History%20Museum&query_place_id=ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
      "scrapedAt": "2026-09-22T23:07:38.397Z",
      "additionalInfo": {
        "Accessibility": [
          {
            "Wheelchair accessible entrance": true
          },
          {
            "Wheelchair accessible parking lot": true
          },
          {
            "Wheelchair accessible restroom": true
          },
          {
            "Wheelchair accessible seating": true
          }
        ]
      },
      "_exampleProvenance": {
        "sourceActorId": "nwua9Gu5YrADL7ZDj",
        "sourceRunId": "5VeVFvxu0MQhPBrWY",
        "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
        "curatedPublicSnapshot": true,
        "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
      }
    },
    {
      "title": "Museum of Illusions",
      "subTitle": null,
      "description": null,
      "ownerDescription": "Come challenge your senses at Museum of Illusions London, an interactive museum and immersive experience located on Tottenham Court Road in the heart of London. Explore a must see attraction that blends museum with illusion rooms, installations and holograms that make it one of the top attractions for anyone looking for fun things to do in London. Perfect for kids activities, family fun, birthday parties, field trips, date night, group activities, teambuilding and corporate events, it is a family friendly museum where educational fun meets unforgettable entertainment. Defy gravity, grow and shrink your body, walk into a life-size kaleidoscope and play tricks on your senses at every turn.",
      "price": null,
      "categoryName": "Museum",
      "address": "6, 17 Tottenham Ct Rd, London W1T 1BJ, United Kingdom",
      "neighborhood": null,
      "street": "6, 17 Tottenham Ct Rd",
      "city": "London",
      "postalCode": "W1T 1BJ",
      "state": null,
      "countryCode": "GB",
      "website": "https://moilondon.uk/?utm_source=google&utm_medium=local&utm_campaign=uberall",
      "phone": "",
      "phoneUnformatted": "",
      "location": {
        "lat": 51.5174418,
        "lng": -0.1316447
      },
      "totalScore": 4.8,
      "reviewsCount": 1015,
      "permanentlyClosed": false,
      "temporarilyClosed": false,
      "placeId": "ChIJsS5McdcbdkgR9rc9sUf1ZS4",
      "cid": "3343347986657556470",
      "url": "https://www.google.com/maps/search/?api=1&query=Museum%20of%20Illusions&query_place_id=ChIJsS5McdcbdkgR9rc9sUf1ZS4",
      "scrapedAt": "2026-09-22T23:07:41.959Z",
      "additionalInfo": {
        "Service options": [
          {
            "Onsite services": true
          }
        ],
        "Accessibility": [
          {
            "Assisted listening devices": true
          },
          {
            "Wheelchair accessible entrance": true
          },
          {
            "Wheelchair accessible restroom": true
          }
        ],
        "Amenities": [
          {
            "Offers tours": true
          },
          {
            "Restroom": true
          },
          {
            "Wi-Fi": true
          },
          {
            "Free Wi-Fi": true
          }
        ],
        "Crowd": [
          {
            "Family-friendly": true
          }
        ],
        "Planning": [
          {
            "Getting tickets in advance recommended": true
          }
        ],
        "Payments": [
          {
            "Admission fee": true
          }
        ],
        "Children": [
          {
            "Discounts for kids": true
          },
          {
            "Family discount": true
          },
          {
            "Good for kids": true
          },
          {
            "Kid-friendly activities": true
          }
        ]
      },
      "_exampleProvenance": {
        "sourceActorId": "nwua9Gu5YrADL7ZDj",
        "sourceRunId": "5VeVFvxu0MQhPBrWY",
        "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
        "curatedPublicSnapshot": true,
        "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
      }
    },
    {
      "title": "Science Museum",
      "subTitle": null,
      "description": "A vast treasury of science & invention, famous for its state-of-the-art interactive exhibits.",
      "ownerDescription": "Free to visit, the Science Museum is a must-see as part of any trip to London, with award-winning exhibitions, iconic objects and stories of incredible scientific achievement",
      "price": null,
      "categoryName": "Museum",
      "address": "Exhibition Rd, South Kensington, London SW7 2DD, United Kingdom",
      "neighborhood": "South Kensington",
      "street": "Exhibition Rd",
      "city": "London",
      "postalCode": "SW7 2DD",
      "state": null,
      "countryCode": "GB",
      "website": "https://www.sciencemuseum.org.uk/",
      "phone": "+44 330 058 0058",
      "phoneUnformatted": "+443300580058",
      "location": {
        "lat": 51.4972216,
        "lng": -0.1767283
      },
      "totalScore": 4.6,
      "reviewsCount": 77054,
      "permanentlyClosed": false,
      "temporarilyClosed": false,
      "placeId": "ChIJP9oAE0MFdkgR3iKGFKZO1SE",
      "cid": "2437941248506864350",
      "url": "https://www.google.com/maps/search/?api=1&query=Science%20Museum&query_place_id=ChIJP9oAE0MFdkgR3iKGFKZO1SE",
      "scrapedAt": "2026-09-22T23:07:42.214Z",
      "additionalInfo": {
        "Service options": [
          {
            "Onsite services": true
          }
        ],
        "Highlights": [
          {
            "Live performances": true
          }
        ],
        "Accessibility": [
          {
            "Wheelchair accessible entrance": true
          },
          {
            "Wheelchair accessible parking lot": true
          },
          {
            "Wheelchair accessible restroom": true
          },
          {
            "Wheelchair accessible seating": true
          }
        ],
        "Offerings": [
          {
            "Gift shop": true
          },
          {
            "Theater": true
          }
        ],
        "Amenities": [
          {
            "Restaurant": true
          },
          {
            "Restroom": true
          },
          {
            "Wi-Fi": true
          },
          {
            "Free Wi-Fi": true
          }
        ],
        "Crowd": [
          {
            "Family-friendly": true
          }
        ],
        "Planning": [
          {
            "Getting tickets in advance recommended": true
          }
        ],
        "Children": [
          {
            "Good for kids": true
          },
          {
            "Has changing table(s)": true
          }
        ],
        "Parking": [
          {
            "Free parking lot": true
          },
          {
            "Paid parking lot": true
          },
          {
            "Paid street parking": true
          }
        ]
      },
      "_exampleProvenance": {
        "sourceActorId": "nwua9Gu5YrADL7ZDj",
        "sourceRunId": "5VeVFvxu0MQhPBrWY",
        "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
        "curatedPublicSnapshot": true,
        "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
      }
    }
  ],
  "maxInputRows": 100,
  "maxResults": 10,
  "minimumReviews": 10,
  "lowRatingThreshold": 3.5,
  "weights": {
    "missingWebsite": 40,
    "lowRating": 30,
    "missingOwnerDescription": 15,
    "sparseListing": 15
  }
}
```

# Actor output Schema

## `results` (type: `string`):

API URL for the default dataset items produced by this run.

## `summary` (type: `string`):

API URL for the OUTPUT record in the default key-value store.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "items": [
        {
            "title": "Natural History Museum",
            "subTitle": null,
            "description": "Landmark museum of animals and natural phenomena, with hands-on exhibits and animatronic dinosaurs.",
            "ownerDescription": null,
            "price": null,
            "categoryName": "Natural history museum",
            "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
            "neighborhood": "South Kensington",
            "street": "Cromwell Rd",
            "city": "London",
            "postalCode": "SW7 5BD",
            "state": null,
            "countryCode": "GB",
            "website": "https://www.nhm.ac.uk/",
            "phone": "+44 20 7942 5000",
            "phoneUnformatted": "+442079425000",
            "location": {
                "lat": 51.496715,
                "lng": -0.1763672
            },
            "totalScore": 4.6,
            "reviewsCount": 39058,
            "permanentlyClosed": false,
            "temporarilyClosed": false,
            "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
            "cid": "8914844151054820548",
            "url": "https://www.google.com/maps/search/?api=1&query=Natural%20History%20Museum&query_place_id=ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
            "scrapedAt": "2026-09-22T23:07:38.397Z",
            "additionalInfo": {
                "Accessibility": [
                    {
                        "Wheelchair accessible entrance": true
                    },
                    {
                        "Wheelchair accessible parking lot": true
                    },
                    {
                        "Wheelchair accessible restroom": true
                    },
                    {
                        "Wheelchair accessible seating": true
                    }
                ]
            },
            "_exampleProvenance": {
                "sourceActorId": "nwua9Gu5YrADL7ZDj",
                "sourceRunId": "5VeVFvxu0MQhPBrWY",
                "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
                "curatedPublicSnapshot": true,
                "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
            }
        },
        {
            "title": "Museum of Illusions",
            "subTitle": null,
            "description": null,
            "ownerDescription": "Come challenge your senses at Museum of Illusions London, an interactive museum and immersive experience located on Tottenham Court Road in the heart of London. Explore a must see attraction that blends museum with illusion rooms, installations and holograms that make it one of the top attractions for anyone looking for fun things to do in London. Perfect for kids activities, family fun, birthday parties, field trips, date night, group activities, teambuilding and corporate events, it is a family friendly museum where educational fun meets unforgettable entertainment. Defy gravity, grow and shrink your body, walk into a life-size kaleidoscope and play tricks on your senses at every turn.",
            "price": null,
            "categoryName": "Museum",
            "address": "6, 17 Tottenham Ct Rd, London W1T 1BJ, United Kingdom",
            "neighborhood": null,
            "street": "6, 17 Tottenham Ct Rd",
            "city": "London",
            "postalCode": "W1T 1BJ",
            "state": null,
            "countryCode": "GB",
            "website": "https://moilondon.uk/?utm_source=google&utm_medium=local&utm_campaign=uberall",
            "phone": "",
            "phoneUnformatted": "",
            "location": {
                "lat": 51.5174418,
                "lng": -0.1316447
            },
            "totalScore": 4.8,
            "reviewsCount": 1015,
            "permanentlyClosed": false,
            "temporarilyClosed": false,
            "placeId": "ChIJsS5McdcbdkgR9rc9sUf1ZS4",
            "cid": "3343347986657556470",
            "url": "https://www.google.com/maps/search/?api=1&query=Museum%20of%20Illusions&query_place_id=ChIJsS5McdcbdkgR9rc9sUf1ZS4",
            "scrapedAt": "2026-09-22T23:07:41.959Z",
            "additionalInfo": {
                "Service options": [
                    {
                        "Onsite services": true
                    }
                ],
                "Accessibility": [
                    {
                        "Assisted listening devices": true
                    },
                    {
                        "Wheelchair accessible entrance": true
                    },
                    {
                        "Wheelchair accessible restroom": true
                    }
                ],
                "Amenities": [
                    {
                        "Offers tours": true
                    },
                    {
                        "Restroom": true
                    },
                    {
                        "Wi-Fi": true
                    },
                    {
                        "Free Wi-Fi": true
                    }
                ],
                "Crowd": [
                    {
                        "Family-friendly": true
                    }
                ],
                "Planning": [
                    {
                        "Getting tickets in advance recommended": true
                    }
                ],
                "Payments": [
                    {
                        "Admission fee": true
                    }
                ],
                "Children": [
                    {
                        "Discounts for kids": true
                    },
                    {
                        "Family discount": true
                    },
                    {
                        "Good for kids": true
                    },
                    {
                        "Kid-friendly activities": true
                    }
                ]
            },
            "_exampleProvenance": {
                "sourceActorId": "nwua9Gu5YrADL7ZDj",
                "sourceRunId": "5VeVFvxu0MQhPBrWY",
                "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
                "curatedPublicSnapshot": true,
                "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
            }
        },
        {
            "title": "Science Museum",
            "subTitle": null,
            "description": "A vast treasury of science & invention, famous for its state-of-the-art interactive exhibits.",
            "ownerDescription": "Free to visit, the Science Museum is a must-see as part of any trip to London, with award-winning exhibitions, iconic objects and stories of incredible scientific achievement",
            "price": null,
            "categoryName": "Museum",
            "address": "Exhibition Rd, South Kensington, London SW7 2DD, United Kingdom",
            "neighborhood": "South Kensington",
            "street": "Exhibition Rd",
            "city": "London",
            "postalCode": "SW7 2DD",
            "state": null,
            "countryCode": "GB",
            "website": "https://www.sciencemuseum.org.uk/",
            "phone": "+44 330 058 0058",
            "phoneUnformatted": "+443300580058",
            "location": {
                "lat": 51.4972216,
                "lng": -0.1767283
            },
            "totalScore": 4.6,
            "reviewsCount": 77054,
            "permanentlyClosed": false,
            "temporarilyClosed": false,
            "placeId": "ChIJP9oAE0MFdkgR3iKGFKZO1SE",
            "cid": "2437941248506864350",
            "url": "https://www.google.com/maps/search/?api=1&query=Science%20Museum&query_place_id=ChIJP9oAE0MFdkgR3iKGFKZO1SE",
            "scrapedAt": "2026-09-22T23:07:42.214Z",
            "additionalInfo": {
                "Service options": [
                    {
                        "Onsite services": true
                    }
                ],
                "Highlights": [
                    {
                        "Live performances": true
                    }
                ],
                "Accessibility": [
                    {
                        "Wheelchair accessible entrance": true
                    },
                    {
                        "Wheelchair accessible parking lot": true
                    },
                    {
                        "Wheelchair accessible restroom": true
                    },
                    {
                        "Wheelchair accessible seating": true
                    }
                ],
                "Offerings": [
                    {
                        "Gift shop": true
                    },
                    {
                        "Theater": true
                    }
                ],
                "Amenities": [
                    {
                        "Restaurant": true
                    },
                    {
                        "Restroom": true
                    },
                    {
                        "Wi-Fi": true
                    },
                    {
                        "Free Wi-Fi": true
                    }
                ],
                "Crowd": [
                    {
                        "Family-friendly": true
                    }
                ],
                "Planning": [
                    {
                        "Getting tickets in advance recommended": true
                    }
                ],
                "Children": [
                    {
                        "Good for kids": true
                    },
                    {
                        "Has changing table(s)": true
                    }
                ],
                "Parking": [
                    {
                        "Free parking lot": true
                    },
                    {
                        "Paid parking lot": true
                    },
                    {
                        "Paid street parking": true
                    }
                ]
            },
            "_exampleProvenance": {
                "sourceActorId": "nwua9Gu5YrADL7ZDj",
                "sourceRunId": "5VeVFvxu0MQhPBrWY",
                "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
                "curatedPublicSnapshot": true,
                "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
            }
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/google-maps-service-opportunity-scorer").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "items": [
        {
            "title": "Natural History Museum",
            "subTitle": None,
            "description": "Landmark museum of animals and natural phenomena, with hands-on exhibits and animatronic dinosaurs.",
            "ownerDescription": None,
            "price": None,
            "categoryName": "Natural history museum",
            "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
            "neighborhood": "South Kensington",
            "street": "Cromwell Rd",
            "city": "London",
            "postalCode": "SW7 5BD",
            "state": None,
            "countryCode": "GB",
            "website": "https://www.nhm.ac.uk/",
            "phone": "+44 20 7942 5000",
            "phoneUnformatted": "+442079425000",
            "location": {
                "lat": 51.496715,
                "lng": -0.1763672,
            },
            "totalScore": 4.6,
            "reviewsCount": 39058,
            "permanentlyClosed": False,
            "temporarilyClosed": False,
            "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
            "cid": "8914844151054820548",
            "url": "https://www.google.com/maps/search/?api=1&query=Natural%20History%20Museum&query_place_id=ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
            "scrapedAt": "2026-09-22T23:07:38.397Z",
            "additionalInfo": { "Accessibility": [
                    { "Wheelchair accessible entrance": True },
                    { "Wheelchair accessible parking lot": True },
                    { "Wheelchair accessible restroom": True },
                    { "Wheelchair accessible seating": True },
                ] },
            "_exampleProvenance": {
                "sourceActorId": "nwua9Gu5YrADL7ZDj",
                "sourceRunId": "5VeVFvxu0MQhPBrWY",
                "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
                "curatedPublicSnapshot": True,
                "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities.",
            },
        },
        {
            "title": "Museum of Illusions",
            "subTitle": None,
            "description": None,
            "ownerDescription": "Come challenge your senses at Museum of Illusions London, an interactive museum and immersive experience located on Tottenham Court Road in the heart of London. Explore a must see attraction that blends museum with illusion rooms, installations and holograms that make it one of the top attractions for anyone looking for fun things to do in London. Perfect for kids activities, family fun, birthday parties, field trips, date night, group activities, teambuilding and corporate events, it is a family friendly museum where educational fun meets unforgettable entertainment. Defy gravity, grow and shrink your body, walk into a life-size kaleidoscope and play tricks on your senses at every turn.",
            "price": None,
            "categoryName": "Museum",
            "address": "6, 17 Tottenham Ct Rd, London W1T 1BJ, United Kingdom",
            "neighborhood": None,
            "street": "6, 17 Tottenham Ct Rd",
            "city": "London",
            "postalCode": "W1T 1BJ",
            "state": None,
            "countryCode": "GB",
            "website": "https://moilondon.uk/?utm_source=google&utm_medium=local&utm_campaign=uberall",
            "phone": "",
            "phoneUnformatted": "",
            "location": {
                "lat": 51.5174418,
                "lng": -0.1316447,
            },
            "totalScore": 4.8,
            "reviewsCount": 1015,
            "permanentlyClosed": False,
            "temporarilyClosed": False,
            "placeId": "ChIJsS5McdcbdkgR9rc9sUf1ZS4",
            "cid": "3343347986657556470",
            "url": "https://www.google.com/maps/search/?api=1&query=Museum%20of%20Illusions&query_place_id=ChIJsS5McdcbdkgR9rc9sUf1ZS4",
            "scrapedAt": "2026-09-22T23:07:41.959Z",
            "additionalInfo": {
                "Service options": [{ "Onsite services": True }],
                "Accessibility": [
                    { "Assisted listening devices": True },
                    { "Wheelchair accessible entrance": True },
                    { "Wheelchair accessible restroom": True },
                ],
                "Amenities": [
                    { "Offers tours": True },
                    { "Restroom": True },
                    { "Wi-Fi": True },
                    { "Free Wi-Fi": True },
                ],
                "Crowd": [{ "Family-friendly": True }],
                "Planning": [{ "Getting tickets in advance recommended": True }],
                "Payments": [{ "Admission fee": True }],
                "Children": [
                    { "Discounts for kids": True },
                    { "Family discount": True },
                    { "Good for kids": True },
                    { "Kid-friendly activities": True },
                ],
            },
            "_exampleProvenance": {
                "sourceActorId": "nwua9Gu5YrADL7ZDj",
                "sourceRunId": "5VeVFvxu0MQhPBrWY",
                "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
                "curatedPublicSnapshot": True,
                "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities.",
            },
        },
        {
            "title": "Science Museum",
            "subTitle": None,
            "description": "A vast treasury of science & invention, famous for its state-of-the-art interactive exhibits.",
            "ownerDescription": "Free to visit, the Science Museum is a must-see as part of any trip to London, with award-winning exhibitions, iconic objects and stories of incredible scientific achievement",
            "price": None,
            "categoryName": "Museum",
            "address": "Exhibition Rd, South Kensington, London SW7 2DD, United Kingdom",
            "neighborhood": "South Kensington",
            "street": "Exhibition Rd",
            "city": "London",
            "postalCode": "SW7 2DD",
            "state": None,
            "countryCode": "GB",
            "website": "https://www.sciencemuseum.org.uk/",
            "phone": "+44 330 058 0058",
            "phoneUnformatted": "+443300580058",
            "location": {
                "lat": 51.4972216,
                "lng": -0.1767283,
            },
            "totalScore": 4.6,
            "reviewsCount": 77054,
            "permanentlyClosed": False,
            "temporarilyClosed": False,
            "placeId": "ChIJP9oAE0MFdkgR3iKGFKZO1SE",
            "cid": "2437941248506864350",
            "url": "https://www.google.com/maps/search/?api=1&query=Science%20Museum&query_place_id=ChIJP9oAE0MFdkgR3iKGFKZO1SE",
            "scrapedAt": "2026-09-22T23:07:42.214Z",
            "additionalInfo": {
                "Service options": [{ "Onsite services": True }],
                "Highlights": [{ "Live performances": True }],
                "Accessibility": [
                    { "Wheelchair accessible entrance": True },
                    { "Wheelchair accessible parking lot": True },
                    { "Wheelchair accessible restroom": True },
                    { "Wheelchair accessible seating": True },
                ],
                "Offerings": [
                    { "Gift shop": True },
                    { "Theater": True },
                ],
                "Amenities": [
                    { "Restaurant": True },
                    { "Restroom": True },
                    { "Wi-Fi": True },
                    { "Free Wi-Fi": True },
                ],
                "Crowd": [{ "Family-friendly": True }],
                "Planning": [{ "Getting tickets in advance recommended": True }],
                "Children": [
                    { "Good for kids": True },
                    { "Has changing table(s)": True },
                ],
                "Parking": [
                    { "Free parking lot": True },
                    { "Paid parking lot": True },
                    { "Paid street parking": True },
                ],
            },
            "_exampleProvenance": {
                "sourceActorId": "nwua9Gu5YrADL7ZDj",
                "sourceRunId": "5VeVFvxu0MQhPBrWY",
                "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
                "curatedPublicSnapshot": True,
                "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities.",
            },
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("zinin/google-maps-service-opportunity-scorer").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "items": [
    {
      "title": "Natural History Museum",
      "subTitle": null,
      "description": "Landmark museum of animals and natural phenomena, with hands-on exhibits and animatronic dinosaurs.",
      "ownerDescription": null,
      "price": null,
      "categoryName": "Natural history museum",
      "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
      "neighborhood": "South Kensington",
      "street": "Cromwell Rd",
      "city": "London",
      "postalCode": "SW7 5BD",
      "state": null,
      "countryCode": "GB",
      "website": "https://www.nhm.ac.uk/",
      "phone": "+44 20 7942 5000",
      "phoneUnformatted": "+442079425000",
      "location": {
        "lat": 51.496715,
        "lng": -0.1763672
      },
      "totalScore": 4.6,
      "reviewsCount": 39058,
      "permanentlyClosed": false,
      "temporarilyClosed": false,
      "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
      "cid": "8914844151054820548",
      "url": "https://www.google.com/maps/search/?api=1&query=Natural%20History%20Museum&query_place_id=ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
      "scrapedAt": "2026-09-22T23:07:38.397Z",
      "additionalInfo": {
        "Accessibility": [
          {
            "Wheelchair accessible entrance": true
          },
          {
            "Wheelchair accessible parking lot": true
          },
          {
            "Wheelchair accessible restroom": true
          },
          {
            "Wheelchair accessible seating": true
          }
        ]
      },
      "_exampleProvenance": {
        "sourceActorId": "nwua9Gu5YrADL7ZDj",
        "sourceRunId": "5VeVFvxu0MQhPBrWY",
        "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
        "curatedPublicSnapshot": true,
        "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
      }
    },
    {
      "title": "Museum of Illusions",
      "subTitle": null,
      "description": null,
      "ownerDescription": "Come challenge your senses at Museum of Illusions London, an interactive museum and immersive experience located on Tottenham Court Road in the heart of London. Explore a must see attraction that blends museum with illusion rooms, installations and holograms that make it one of the top attractions for anyone looking for fun things to do in London. Perfect for kids activities, family fun, birthday parties, field trips, date night, group activities, teambuilding and corporate events, it is a family friendly museum where educational fun meets unforgettable entertainment. Defy gravity, grow and shrink your body, walk into a life-size kaleidoscope and play tricks on your senses at every turn.",
      "price": null,
      "categoryName": "Museum",
      "address": "6, 17 Tottenham Ct Rd, London W1T 1BJ, United Kingdom",
      "neighborhood": null,
      "street": "6, 17 Tottenham Ct Rd",
      "city": "London",
      "postalCode": "W1T 1BJ",
      "state": null,
      "countryCode": "GB",
      "website": "https://moilondon.uk/?utm_source=google&utm_medium=local&utm_campaign=uberall",
      "phone": "",
      "phoneUnformatted": "",
      "location": {
        "lat": 51.5174418,
        "lng": -0.1316447
      },
      "totalScore": 4.8,
      "reviewsCount": 1015,
      "permanentlyClosed": false,
      "temporarilyClosed": false,
      "placeId": "ChIJsS5McdcbdkgR9rc9sUf1ZS4",
      "cid": "3343347986657556470",
      "url": "https://www.google.com/maps/search/?api=1&query=Museum%20of%20Illusions&query_place_id=ChIJsS5McdcbdkgR9rc9sUf1ZS4",
      "scrapedAt": "2026-09-22T23:07:41.959Z",
      "additionalInfo": {
        "Service options": [
          {
            "Onsite services": true
          }
        ],
        "Accessibility": [
          {
            "Assisted listening devices": true
          },
          {
            "Wheelchair accessible entrance": true
          },
          {
            "Wheelchair accessible restroom": true
          }
        ],
        "Amenities": [
          {
            "Offers tours": true
          },
          {
            "Restroom": true
          },
          {
            "Wi-Fi": true
          },
          {
            "Free Wi-Fi": true
          }
        ],
        "Crowd": [
          {
            "Family-friendly": true
          }
        ],
        "Planning": [
          {
            "Getting tickets in advance recommended": true
          }
        ],
        "Payments": [
          {
            "Admission fee": true
          }
        ],
        "Children": [
          {
            "Discounts for kids": true
          },
          {
            "Family discount": true
          },
          {
            "Good for kids": true
          },
          {
            "Kid-friendly activities": true
          }
        ]
      },
      "_exampleProvenance": {
        "sourceActorId": "nwua9Gu5YrADL7ZDj",
        "sourceRunId": "5VeVFvxu0MQhPBrWY",
        "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
        "curatedPublicSnapshot": true,
        "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
      }
    },
    {
      "title": "Science Museum",
      "subTitle": null,
      "description": "A vast treasury of science & invention, famous for its state-of-the-art interactive exhibits.",
      "ownerDescription": "Free to visit, the Science Museum is a must-see as part of any trip to London, with award-winning exhibitions, iconic objects and stories of incredible scientific achievement",
      "price": null,
      "categoryName": "Museum",
      "address": "Exhibition Rd, South Kensington, London SW7 2DD, United Kingdom",
      "neighborhood": "South Kensington",
      "street": "Exhibition Rd",
      "city": "London",
      "postalCode": "SW7 2DD",
      "state": null,
      "countryCode": "GB",
      "website": "https://www.sciencemuseum.org.uk/",
      "phone": "+44 330 058 0058",
      "phoneUnformatted": "+443300580058",
      "location": {
        "lat": 51.4972216,
        "lng": -0.1767283
      },
      "totalScore": 4.6,
      "reviewsCount": 77054,
      "permanentlyClosed": false,
      "temporarilyClosed": false,
      "placeId": "ChIJP9oAE0MFdkgR3iKGFKZO1SE",
      "cid": "2437941248506864350",
      "url": "https://www.google.com/maps/search/?api=1&query=Science%20Museum&query_place_id=ChIJP9oAE0MFdkgR3iKGFKZO1SE",
      "scrapedAt": "2026-09-22T23:07:42.214Z",
      "additionalInfo": {
        "Service options": [
          {
            "Onsite services": true
          }
        ],
        "Highlights": [
          {
            "Live performances": true
          }
        ],
        "Accessibility": [
          {
            "Wheelchair accessible entrance": true
          },
          {
            "Wheelchair accessible parking lot": true
          },
          {
            "Wheelchair accessible restroom": true
          },
          {
            "Wheelchair accessible seating": true
          }
        ],
        "Offerings": [
          {
            "Gift shop": true
          },
          {
            "Theater": true
          }
        ],
        "Amenities": [
          {
            "Restaurant": true
          },
          {
            "Restroom": true
          },
          {
            "Wi-Fi": true
          },
          {
            "Free Wi-Fi": true
          }
        ],
        "Crowd": [
          {
            "Family-friendly": true
          }
        ],
        "Planning": [
          {
            "Getting tickets in advance recommended": true
          }
        ],
        "Children": [
          {
            "Good for kids": true
          },
          {
            "Has changing table(s)": true
          }
        ],
        "Parking": [
          {
            "Free parking lot": true
          },
          {
            "Paid parking lot": true
          },
          {
            "Paid street parking": true
          }
        ]
      },
      "_exampleProvenance": {
        "sourceActorId": "nwua9Gu5YrADL7ZDj",
        "sourceRunId": "5VeVFvxu0MQhPBrWY",
        "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
        "curatedPublicSnapshot": true,
        "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
      }
    }
  ]
}' |
apify call zinin/google-maps-service-opportunity-scorer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zinin/google-maps-service-opportunity-scorer"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Mw6Fno8qkRJUzgylV/builds/TSS2dcFhiFy9Nag0E/openapi.json
