# Apple Maps (Mapas) Scraper - Places, Ratings, Phone + Website (`lanternlab318/apple-maps`) Actor

Search Apple Maps by keyword and city: public places with Apple place ID, name, category, address, coordinates, business phone and website, and aggregate rating with its source. No login, no cookies. Empty terms and status rows are free.

- **URL**: https://apify.com/lanternlab318/apple-maps.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (community)
- **Categories:** Lead generation, Business, Travel
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.19 / 1,000 places

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Apple Maps (Mapas) Scraper — Places, Ratings, Phone + Website

A Lantern Lab Actor. Search Apple Maps by keyword and city and get the public place data back: Apple's own place ID, name, category path, full address, coordinates, the business's own published phone and website, and the aggregate rating with the source it comes from. No Apple ID, no login, no cookies, no account of your own.

**Price:** pay per result — **USD 3.19 per 1,000 places, and no run-start fee.** Empty search terms, per-term status rows and the run summary are free, so a term that found nothing and a term that was cut short cost you nothing. A place whose rating you asked for and whose own page did not come back is free too: it is written with `partial: true` and `partialReason: "rating_unavailable"`, and is not charged.

### What you get

```json
{
  "recordType": "place",
  "placeId": "1241241539567035924",
  "name": "Café com Quintal",
  "category": "Cafe",
  "categories": [
    "Jantar",
    "Compras",
    "Café",
    "Mercado de Comida",
    "Loja de Suprimentos de Café e Chá"
  ],
  "address": "Rua Eça de Queiroz, 122, Vila Mariana, São Paulo - SP, 04011-030, Brasil",
  "addressLines": [
    "Rua Eça de Queiroz, 122",
    "Vila Mariana",
    "São Paulo - SP",
    "04011-030",
    "Brasil"
  ],
  "locality": "São Paulo",
  "subLocality": "Vila Mariana",
  "administrativeArea": "São Paulo",
  "postCode": "04011-030",
  "country": "Brasil",
  "countryCode": "BR",
  "latitude": -23.5783492,
  "longitude": -46.6419572,
  "phone": "+5511998493602",
  "additionalPhones": [
    "+5511985788356"
  ],
  "website": "https://www.facebook.com/cafecomquintal",
  "rating": 2.4,
  "ratingType": "USER_RATING",
  "ratingScore": 2.4,
  "ratingMaxScore": 5,
  "reviewCount": 11,
  "ratingSource": "com.tripadvisor",
  "ratingSourceName": "Tripadvisor",
  "ratingSourceUrl": "http://www.tripadvisor.com/AppleMapsAction?id=23595545",
  "timezone": "America/Sao_Paulo",
  "url": "https://maps.apple.com/place?q=Caf%C3%A9%20com%20Quintal&auid=1241241539567035924…",
  "searchQuery": "cafe",
  "location": "São Paulo, Brazil",
  "rank": 5,
  "searchPointLatitude": -23.5796404,
  "searchPointLongitude": -46.6550645,
  "detailStatus": "ok",
  "publishedAt": null,
  "partial": false,
  "partialReason": null,
  "collectedAt": "2026-10-24T05:29:07.847Z"
}
```

### Input

```json
{
  "searchQueries": ["cafe", "padaria"],
  "location": "São Paulo, Brazil",
  "maxPlaces": 50,
  "includeRating": true,
  "language": "pt-BR"
}
```

| Field | Default | What it does |
| --- | --- | --- |
| `searchQueries` | — | Search terms, one per line. Use the local language where you can: `ร้านกาแฟ` returns more Thai coffee shops than `coffee shop`. |
| `location` | — | City, district or address to search in. Apple Maps resolves it first, and the run reports what it resolved to. |
| `maxPlaces` | `50` | Upper bound on the places one term may produce, up to 300. |
| `includeRating` | `true` | Read each place's own page for its rating and its fuller phone and website. One extra request per place. |
| `language` | — | BCP 47 tag Apple should answer in, for example `pt-BR`, `th-TH`, `id-ID`, `es-MX`. |
| `maxUpstreamRequests` | derived | Hard ceiling on requests to Apple Maps. Left empty, the Actor derives one from your input, so a run is never unbounded. |
| `proxyConfiguration` | Apify residential | Where the requests come from. |

### Output

Three row types in one dataset:

- `place` — one per place, deduplicated across every search term in the run. The only charged row, and only when it is complete — a row with `partial: true` is free.
- `input_status` — one per search term: what it returned, how many of its place rows were charged (`placesCharged`), how many places an earlier term had already collected, how many search points were used, and whether it was cut short. Never charged.
- `RUN_SUMMARY` — the last row, and the same object under the `RUN_SUMMARY` key in the run's key-value store.

#### Two rating scales, never mixed

Apple returns two different scales in the same field:

- `USER_RATING` is a third party's star rating — `ratingScore` out of `ratingMaxScore` 5, with `ratingSource` naming the provider (Foursquare, Tripadvisor).
- `APPLE_RATING` is Apple's own recommendation rate — `ratingScore` out of `ratingMaxScore` **100**.

`rating` is filled **only** from a five-point scale. A place Apple scores 87 of 100 has `rating: null`, `ratingScore: 87`, `ratingMaxScore: 100` — putting 87 into a star column would be fabricated data. Sort and filter on `rating` for stars, on `ratingScore` within one `ratingType`.

#### Empty is not the same as incomplete

A term Apple Maps answered with no place gets a status row with `partial: false` and a sentence saying it is an empty result. A term that was refused, failed, or stopped at a limit gets `partial: true` with a `partialReason` — `blocked`, `upstream_error`, `upstream_request_cap`, `charge_limit_reached`, `unreadable_response`, `location_not_found` or `not_collected` — and is never reported as zero results. Rows written before the stop are kept.

A **place** row is partial for one reason only: `rating_unavailable`. With `includeRating: true`, the rating comes from the place's own page, and when that page is refused, fails, comes back unreadable, or is never asked for because the run hit its request ceiling (`detailStatus` `blocked`, `request_failed`, `unreadable_response` or `upstream_request_cap`), the place is still written — with `rating: null`, `partial: true`, `partialReason: "rating_unavailable"` — and **is not charged**. Two cases that are *not* partial and are charged as usual: a page that was read and simply publishes no rating (`detailStatus: "ok"`, `rating: null`), and `includeRating: false`, where no rating was ever asked for (`detailStatus: "not_requested"`, same price).

### Limits

Measured on real runs on 2026-10-24: 40 places in São Paulo (`pt-BR`), 11 in Bangkok (`th-TH`), plus an anonymous probe across Brazil, Mexico, Thailand, Malaysia, Indonesia and Vietnam.

- **10 places per search, no paging.** Apple serves at most 10 places per search request and honours no page parameter, so a term is searched from several points spread across the location until it has your `maxPlaces`. A row count below `maxPlaces` is what those points returned, not a count of everything Apple Maps holds — the status row says how many points were used.
- **Not every place has a rating.** In the São Paulo run 24 of 40 places carried one, and 19 of those were on the five-point scale; in Bangkok 9 of 11. Coverage differs sharply by country: the probe found nearly every place rated in Mexico and Brazil, about two thirds in Thailand and Malaysia, and none at all in the Indonesian and Vietnamese samples. A place with no rating is complete with `rating: null`; it is not marked partial.
- **Phone and website are not universal either.** São Paulo run: 34 of 40 places published a phone, 22 a website.
- **The location is matched loosely.** Apple Maps resolves `location` to the nearest thing it can find rather than failing: `Qqqzzxw Nowhereville 99999` resolved to Hawlawji, Ghana, and the run searched there. Every status row and the run summary carry `resolvedLocation` — check it before trusting a city's rows.
- **Local-language coverage is thinner in South-East Asia than in Latin America.** A Vietnamese-language term returned one place where a Portuguese one returned ten.
- **Review text and reviewer names are not collected.** Only the aggregate rating, with its source, which Apple requires to be shown alongside the score (`ratingSource`, `ratingSourceUrl`).
- **A term that is itself a place name in the search language collapses to that one place.** `restaurante` with `language: "es-MX"` returns the single place named "Restaurante", while `restaurantes`, `taquería`, or the same term with no `language` return ten POIs each. The run never rewrites your term - the status row shows 1 place from N search points, which is the signal to use a more specific or plural term.
- **A nonsense keyword returns one loose match, not nothing.** Read `placesReturned` together with the status row's sentence, not the row count alone.
- **Some exits are served an Apple Maps page without the MapKit token in it.** The run asks again from a fresh exit, up to three times, and only then reports the term as incomplete with `unreadable_response` — one run in four hit that variant on 2026-10-24 before the retry existed.
- **A single place page is sometimes refused or times out.** In the two runs 3 of 50 place pages did not come back (HTTP 403 once, a transport failure twice). Those places are still written, with `rating: null` and `detailStatus` saying which happened — `blocked`, `request_failed`, `not_found` or `upstream_request_cap`. **You are not charged for them:** with `includeRating: true`, a place row whose page was refused, failed, was unreadable, or was never asked for at the request ceiling carries `partial: true` with `partialReason: "rating_unavailable"` and is free. (`not_found` — Apple answers 404 for the place's own page — is charged: there is no rating to get rather than one we failed to get.)
- Apple Maps publishes no creation time for a place, so `publishedAt` is always `null`; `collectedAt` is our own observation time.

### Development

```bash
npm install
npm test            # offline, against real recorded responses
```

Tests run against fixtures recorded from real Apple Maps responses: the place pages are cut to the two components the Actor reads with their values verbatim, the two token values are redacted, and `search_empty.json` is a real response with its results array emptied.

# Actor input Schema

## `searchQueries` (type: `array`):

One Apple Maps search term per line, in the local language where you can - "ร้านกาแฟ" returns more Thai coffee shops than "coffee shop" does. Each term is searched separately and its rows are labelled with it. A place that matches several of your terms is returned once and charged once.

## `location` (type: `string`):

The city, district or address to search in: "São Paulo", "Bangkok", "Jakarta Selatan", "Ciudad de México". Apple Maps resolves it first, and the run reports what it resolved to. A city is searched across its whole area; a street address is searched from that one point.

## `maxPlaces` (type: `integer`):

Upper bound on the places one search term may produce. Apple Maps serves at most 10 places per search and offers no second page, so a term is searched from several points across the location until it has this many. More points cost more requests, which is why this is capped at 300.

## `includeRating` (type: `boolean`):

The search result carries name, category, full address, coordinates and often the business phone and website - but no rating. With this on, each place's own public page is read for its aggregate rating (and for the fuller phone and website Apple shows there), which is one extra request per place against one request per 10 places for the search itself. Turning it off makes a run about 10x cheaper in requests and leaves every rating null. About 7 in 10 places carry a rating at all; the rest are null with this on.

## `language` (type: `string`):

Optional BCP 47 language tag Apple Maps should answer in, for example "pt-BR", "th-TH", "id-ID", "es-MX". It decides the language of names, categories and addresses. Leave empty to let Apple choose from the exit IP.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the HTTP requests this run sends to Apple Maps: the two token requests, the location lookup, every search point, every place page and every retry. At the ceiling collection stops - the terms not reached are written with partialReason "upstream_request_cap" and the run then ends normally. Leave empty and the Actor works a ceiling out from your inputs and limits, so a run is never unbounded.

## `proxyConfiguration` (type: `object`):

The proxy the requests to Apple Maps go through. Residential exits are the default, so a search is answered the way it would be in the country you are collecting. A run that is refused anyway reports blocked with the refusing response's own signature, never zero results.

## Actor input object example

```json
{
  "searchQueries": [
    "cafe"
  ],
  "location": "São Paulo, Brazil",
  "maxPlaces": 50,
  "includeRating": true,
  "language": "pt-BR",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

No description

## `runSummary` (type: `string`):

No description

# 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 = {
    "searchQueries": [
        "cafe"
    ],
    "location": "São Paulo, Brazil",
    "language": "pt-BR"
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/apple-maps").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 = {
    "searchQueries": ["cafe"],
    "location": "São Paulo, Brazil",
    "language": "pt-BR",
}

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/apple-maps").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 '{
  "searchQueries": [
    "cafe"
  ],
  "location": "São Paulo, Brazil",
  "language": "pt-BR"
}' |
apify call lanternlab318/apple-maps --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lanternlab318/apple-maps"
        }
    }
}
```

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/HMAPrHMz1qbzC279p/builds/hdyN5eD5XsWPdky0J/openapi.json
