# Local Business Scraper — fill-rate report (`rankfabrik/local-business-scraper`) Actor

Search a term and a city to get clean local-business records: verified name, full address, exact coordinates and a direct link on essentially every row. The real fill-rate of phone, website and rating is measured and published for every run, before you pay.

- **URL**: https://apify.com/rankfabrik/local-business-scraper.md
- **Developed by:** [RankFabrik Team](https://apify.com/rankfabrik) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 business rows

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Local Business Scraper — name, address, coordinates and a direct link on every row

Point it at a search term and a city, and it returns clean, ready-to-use
business records: a verified name, a full address, exact coordinates and a
direct link on essentially every row. The exact fill-rate is printed at the
bottom of this page, because it is published for every run.

The fields that actually decide whether a lead is worth calling (phone,
website, rating) are never guaranteed by the source, and most scrapers let you
find that out only after you have paid. This one measures the real fill-rate of
those fields and shows it to you **before** you commit your budget.

> **Unofficial tool — not affiliated.** This is an independent tool. It is not
> affiliated with, endorsed by, or sponsored by Google, Google Maps, or any
> other third-party service. All product names, logos and brands are the
> property of their respective owners and are used for identification only.

> **Public data only.** It reads solely the public, logged-out channel of the
> source: no account, no login, no session, and no circumvention of any
> technical protection or anti-bot measure. It accesses only what is already
> visible to any anonymous visitor.

***

### Reviews, and the legal footing

Reviews are handled cautiously. The source's terms (updated 14 January 2026)
exclude commercial reuse of review text, so we don't collect or resell it: a
legally shaky field isn't worth the row, however tempting it would be to
include. That is why `reviewsCount` stays a count, never the review bodies.

### You see the fill-rate before you pay, not after

Most scrapers work in the dark: you pay, you run, and only then do you learn
how many rows actually carry a phone number or a website. This one turns that
around. Every run reports the live fill-rate of `phone`, `website` and
`rating` in two places — the run's status message while it works, and a
`batchCompleteness` block on every single row. So you can start with a small
run, read the real numbers for your exact search, and scale up only when they
look worth it. And if a request produces nothing at all, it is never billed —
see *Pricing*.

***

### What you get

One row per business, deduplicated on the place identifier (never on the name,
so two branches of the same chain both survive):

| Field | Notes |
|---|---|
| `id` | stable place identifier |
| `name` | business name |
| `address` | full street line when available, composed line otherwise |
| `latitude`, `longitude` | exact coordinates |
| `category` / `categories` | primary category, then up to 10 |
| `rating` | rating, or `null` — see *Completeness, measured* below |
| `reviewsCount` | review count, or `null` when the source omits it |
| `phone` | international or national format, as published |
| `website` | the business's own site — never a map or tracking URL |
| `url` | direct link to the place |
| `enriched` | `true` when both phone and website are present |
| `batchCompleteness` | the completeness percentages of this run |

**No field is ever inferred.** If the source does not publish a phone number,
you get a clean `null` — never a plausible-looking number invented by a
language model to fill the gap. That is a deliberate choice: a made-up field
looks like data right up until it costs you a wasted call, and inferred
contact details are the single most common complaint about tools in this
category. We would rather hand you an honest blank.

The `batchCompleteness` percentages mirror exactly what the source publishes,
not what we drop. A `0%` means the source itself simply does not expose that
field for those listings (Berlin is the measured example further down) — and
because you read that figure before you pay, an empty field is never a
surprise you discover after the run.

***

### Example output — one row, illustrative

```json
{
  "id": "<stable place id>",
  "name": "Example Dental Office",
  "address": "12 Example Ave, New York, NY 10001, USA",
  "latitude": 40.7128,
  "longitude": -74.0060,
  "category": "Dental clinic",
  "categories": ["Dental clinic", "Dentist"],
  "rating": 4.6,
  "reviewsCount": 128,
  "phone": "+1 212 000 0000",
  "website": "https://example-dental.com",
  "url": "https://www.google.com/maps?cid=0000000000000000000",
  "enriched": true,
  "batchCompleteness": {
    "name": 100,
    "address": 98,
    "coordinates": 100,
    "rating": 91,
    "phone": 74,
    "website": 61,
    "category": 99,
    "todayHours": 55
  }
}
```

`batchCompleteness` is identical on every row of a given run — read it once and
you know the fill-rate of the whole batch, not just this line.

***

### Input

| Field | Required | Default | Meaning |
|---|---|---|---|
| `searchTerm` | yes | — | what you're looking for, in plain words |
| `city` | one of the two | `paris` | 16 preloaded cities |
| `latitude` + `longitude` | one of the two | — | any other point on earth |
| `radiusKm` | no | 4 | half-width of the swept area |
| `gridSize` | no | 3 | grid fineness: 1, 3×3=9, up to 6×6=36 points |
| `listDepth` | no | 20 | pages read per point: 1 = first page, 20 = to the bottom |
| `maxResults` | no | 500 | hard ceiling on delivered (and billed) rows |

Example:

```json
{
  "searchTerm": "dental office",
  "city": "new york",
  "radiusKm": 6,
  "gridSize": 4,
  "listDepth": 20,
  "maxResults": 400
}
```

***

### Pricing

Pay per result, in two tiers, because not all rows are worth the same to you:

| Event | Price | What it is |
|---|---|---|
| `place` | $1.00 per 1,000 rows | a business row |
| `place-contact` | $2.00 per 1,000 rows | a row with **both** phone and website |

You are charged per row written to your dataset, and for nothing else. Failed
runs, refused requests and empty areas cost you zero. Your own spending cap is
enforced by the platform — the run stops cleanly when it is reached, mid-dataset,
without burning compute you did not authorise.

***

### How the search works, and what to expect

A single map point returns at most 20 businesses, so density comes from the
**grid**, not from a bigger page. `radiusKm` sets how wide an area you sweep,
`gridSize` sets how finely it is divided (1 point, a 3×3 grid of 9, up to a 6×6
grid of 36), and `listDepth` sets how deep you page each point. More points and
more pages mean denser coverage of the same area. The full completeness picture
that comes back, including the honest 0%, is published up front so you know what
to expect before you buy.

- **Reviews are not included.** `reviewsCount` is a review *count*, not review text. The
  review bodies are not available through this channel, and the source's terms
  (updated 14 January 2026) exclude commercial reuse. We do not sell what we
  cannot deliver lawfully.
- **Ratings are not uniform by city.** Measured, `rating` coverage:

  | City | `rating` coverage |
  |---|---|
  | Paris, Lyon, New York, London, Marrakech | ~100% |
  | Madrid | ~50% |
  | Berlin | **0%** |
  | Tokyo | flips between runs |

  This is a property of the source, not of the tool — and it is exactly what
  `batchCompleteness` exists to tell you, run by run.
- **An empty result is never charged.** If the source refuses the request, the
  run reports the reason and bills you nothing.

***

### Prefer a hosted API with a flat monthly plan?

This actor bills per result on the Apify platform. If you'd rather call a hosted
endpoint with your own key, a flat monthly quota, and the same
completeness block on every response, the same data ships as a standalone API:

- **Places API** — one flat unit per enriched business record. See the full
  pricing breakdown:
  <https://rankfabrik.com/google-places-api-pricing>
- Sibling APIs on the same principle:
  [Jobs](https://rankfabrik.com/jobs-api-pricing) (past the 1,000-result cap) and
  [YouTube transcripts](https://rankfabrik.com/captions-api-pricing)
  (human-vs-auto caption transparency).

One engine feeds both; the measured fill rate is identical. You pick the
billing model that fits.

***

### Legal & responsible use

**Your responsibility as the user.** You are solely responsible for how you use
the data this tool returns, including compliance with applicable laws, the
source's terms, data-protection rules (GDPR/CCPA), and third-party rights. We
provide a data-access tool and make no representation that any particular use is
lawful in your jurisdiction.

**Personal data notice.** Business contact details such as phone numbers may
constitute personal data under the GDPR even when publicly available. If you
process them you act as the data controller: you must have a lawful basis (for
B2B outreach, typically legitimate interest, Art. 6(1)(f)), inform data subjects
where required (Art. 14), and honor objection and erasure requests (Arts. 21, 17).

**Removal requests.** If you are a data subject or a rights holder and want a
specific record removed, contact contact@rankfabrik.com. Justified requests are
processed promptly (target: within 30 days).

***

### Support

Questions and bug reports go through this actor's **Issues** tab. If a run
returned something you think is wrong, quote the run: every execution logs its
completeness figures, so the conversation starts from the same numbers you saw.

# Actor input Schema

## `searchTerm` (type: `string`):

The kind of business, written the way you'd say it: "dental office", "italian restaurant", "auto repair shop".

## `city` (type: `string`):

Sixteen cities are preloaded. For anywhere else, leave this empty and provide latitude and longitude instead.

## `latitude` (type: `string`):

Center of the sweep. Example: 40.7128

## `longitude` (type: `string`):

Center of the sweep. Example: -74.0060

## `radiusKm` (type: `integer`):

Half-width of the area swept around the center point.

## `gridSize` (type: `integer`):

The area is swept by a grid of size × size points. 1 = a single point (20 results at most), 3 = nine points, 6 = thirty-six. Finer = more complete and more expensive. Deduplication is on the place identifier, never on the name.

## `listDepth` (type: `integer`):

The source returns twenty results per call, but its list holds far more — you page through it. 1 = first page only (fastest), 20 = to the bottom of the list. Measured on one area: 152 businesses at 1 page, 887 at 20 pages, in sixteen seconds. Grid and depth multiply — they don't replace each other.

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

Hard ceiling on the number of rows delivered — and therefore billed.

## Actor input object example

```json
{
  "searchTerm": "dental office",
  "city": "paris",
  "radiusKm": 4,
  "gridSize": 3,
  "listDepth": 20,
  "maxResults": 500
}
```

# Actor output Schema

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

All business rows written to the default dataset.

# 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 = {
    "searchTerm": "dental office"
};

// Run the Actor and wait for it to finish
const run = await client.actor("rankfabrik/local-business-scraper").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 = { "searchTerm": "dental office" }

# Run the Actor and wait for it to finish
run = client.actor("rankfabrik/local-business-scraper").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 '{
  "searchTerm": "dental office"
}' |
apify call rankfabrik/local-business-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rankfabrik/local-business-scraper"
        }
    }
}

```

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/JG7I5PJ0WM3ko3hRr/builds/Ni64Q3FZG4FS7rffA/openapi.json
