# Google Maps Lead Scraper & SMB Audit (`syzen/apify`) Actor

- **URL**: https://apify.com/syzen/apify.md
- **Developed by:** [Muhammad Sajid Izzulhaq](https://apify.com/syzen) (community)
- **Stats:** 2 total users, 1 monthly users, 90.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

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

## UMKM Lead Generator — Google Maps

Actor Apify yang mencari **UMKM yang belum punya website**, lalu memberi skor
urutan prioritas supaya Anda tahu siapa yang perlu dihubungi lebih dulu.

Cocok untuk siapa: freelance web developer, digital agency, dan siapa pun yang
menj jasa pembuatan website atau landing page ke UMKM.

### Masalah yang diselesaikan

Mencari calon klien secara manual di Google Maps itu melelahkan. Kalau Anda
cari "warung kopi", rata-rata 19 dari 20 hasil **tidak punya website sama
sekali** — dan sebagian besar tidak punya nomor telepon yang bisa dihubungi.
Actor ini menyaring dua masalah itu sekaligus, lalu menandai mana yang paling
layak dikejar.

### Yang dilakukan actor

1. Mengambil hasil pencarian Google Maps lewat endpoint JSON internal
   (`tbm=map`). Tanpa headless browser, jadi jauh lebih cepat dan murah.
2. Mengekstrak nama, alamat, rating, jumlah ulasan, kategori, koordinat,
   **nomor telepon**, dan website.
3. Menghitung `gapScore` 0-100 untuk setiap bisnis, dengan `gapReason` dalam
   Bahasa Indonesia yang bisa langsung Anda pakai saat menelepon.
4. Mengembalikan hanya lead yang layak: tidak punya website sendiri **dan**
   punya nomor telepon publik.

### Input

`location` itu bukan hiasan: nama kota di situ **menggeser pusat pencarian**.
Filter geografis Google ada di dalam `pb` pada `!2d{lng}!3d{lat}`, jadi kalau
kotanya tidak dipetakan ke koordinat yang benar, hasilnya salah kota tanpa
warning.

Yang dilakukan actor dengan `location`:

1. dicocokkan ke tabel kota offline (819 nama: 217 kota Indonesia + kota besar
   dunia), case/spasi/awalan `kota` tidak berpengaruh;
2. dipakai sebagai titik tengah pencarian, bukan cuma tambahan di query;
3. dipakai juga untuk memperingatkan kalau `countryCode` tidak cocok.

Kalau nama kotanya tidak ada di tabel, actor **berhenti** dengan pesan jelas —
lebih baik gagal daripada mengirim lead salah kota. Wilayah yang ada di tabel
melewati `lat`/`lng`, atau tambahkan kotanya ke
`packages/gmaps-core/src/gmaps_core/data/cities.json`.

| Field | Tipe | Default | Keterangan |
|---|---|---|---|
| `searchStrings` | list of string | `["Kopi"]` | Kata kunci. Actor menggabungkannya dengan `location`. |
| `location` | string | **wajib** | Kota yang jadi pusat pemindaian, mis. `Jakarta`. Dicocokkan ke tabel kota offline. |
| `lat` / `lng` | number | kosong | Titik tengah manual. Kosongkan kecuali tahu persis koordinatnya. |
| `radiusKm` | number | `30` | Radius dari titik tengah. |
| `maxItems` | integer | `20` | Total hasil per kata kunci (1-480). Dipecah otomatis jadi beberapa request. |
| `language` | string | `id` | Kode bahasa respons Google, mis. `id` atau `en`. |
| `countryCode` | string | `id` | Kode negara respons Google, mis. `id` atau `jp`. |
| `onlyGapLeads` | boolean | `true` | Hanya lead tanpa website + bertelepon. |
| `minGapScore` | integer | `0` | Buang lead di bawah skor ini. |
| `includeUnproven` | boolean | `false` | Sertakan bisnis tanpa rating. |
| `includeRaw` | boolean | `false` | Kembalikan semua tempat, bukan hanya lead. |
| `debugDump` | boolean | `false` | Simpan payload Google mentah ke key-value store. |

### Output

Tiga keluaran sekaligus:

| Tujuan | Lokasi di halaman run | Isi |
|---|---|---|
| Spreadsheet | key-value store default, record `leads.xlsx` | 2 sheet: `Leads` dan `Ringkasan`. |
| Data mentah | dataset | Satu baris per tempat, bisa disaring di UI Apify. |
| Ringkasan run | key-value store default, record `SUMMARY` | Statistik dan parameter run. |

Dataset punya dua tampilan, dipilih dari dropdown di atas tabel:

| Tampilan | Isi |
|---|---|
| `Leads` | 15 kolom untuk menghubungi calon klien, urut dari skor tertinggi. |
| `Semua tempat` | 24 kolom lengkap, termasuk yang sudah punya website sendiri. |

Setiap kolom punya judul Bahasa Indonesia, dan `Prioritas` memakai nilai
`Panas` / `Hangat` / `Dingin` — bukan `hot` / `warm` / `cold` yang cuma
penting di kode.

#### Isi spreadsheet

Sheet `Leads` memuat semua tempat yang dipindai (bukan hanya lead), diurutkan
sesuai skor. Handy karena yang butuh filtering bisa pakai dropdown Autofilter:

`No`, `Nama`, `Kategori`, `Rating`, `Ulasan`, `Telepon`, `WhatsApp`, `Kota`,
`Kecamatan`, `Alamat`, `Website`, `Skor Gap`, `Prioritas`,
`Alasan (script jualan)`, `Buka di Maps`, `Penawaran`.

Beberapa detail yang disengaja:

- **Telepon ditulis sebagai teks.** Kalau jadi angka, Excel membuang nol di
  depan dan nomor jadi tidak bisa dihubungi.
- **Kolom WhatsApp** adalah tautan `wa.me` siap klik, tapi hanya untuk nomor
  HP. Nomor tetap tidak bisa dipakai WhatsApp, jadi dikosongkan.
- **Alasan dan Penawaran** sudah Bahasa Indonesia, bisa langsung dibacakan
  saat menelepon.

Sheet `Ringkasan` berisi konfigurasi run, jumlah lead per prioritas, dan
seberapa banyak yang punya nomor telepon.

#### Field di dataset

| Field | Keterangan |
|---|---|
| `title`, `address`, `rating`, `review_count`, `category` | Data dasar bisnis. |
| `phone`, `phoneInternational`, `phoneDigits` | Nomor telepon dalam tiga format. |
| `website` | URL website kalau ada. |
| `websiteKind` | `own` (punya website sendiri), `social` (cuma akun social), `none`. |
| `gapScore` | 0-100. Makin tinggi makin layak dikejar. |
| `gapTier` | `hot` / `warm` / `cold` / `none`. |
| `isGapLead` | `true` bila layak dihubungi untuk jasa website. |
| `gapReason` | Alasan Bahasa Indonesia, siap dibacakan ke prospek. |
| `suggestedOffer` | Rekomendasi materi penawaran. |
| `place_url`, `reviews_url`, `lat`, `lng` | Untuk verifikasi manual. |

### Cara skor dihitung

Tiga komponen, semuanya transparan:

| Komponen | Nilai | Logika |
|---|---|---|
| Celah digital | 0-40 | Tidak ada web (40), cuma social (32), sudah punya web (0). |
| Reputasi | 0-47 | Rating hanya dihitung di atas 3,5; ulasan memakai skala logaritmik. |
| Bisa dihubungi | 0-15 | Punya nomor telepon publik. |

Dua penyesuaian yang disengaja:

- **Rating di bawah 3,5 dapat 0 poin.** Bisnis lemah adalah risiko, bukan
  peluang.
- **Ulasan sangat banyak (>3.000) dikurangi 8 poin.** Hal tersebut sudah
  bercabang, sehingga lebih sulit dijual jasa website pribadi.

### Contoh output

```json
{
  "no": 1,
  "title": "BARBIE LAUNDRY",
  "address": "Jl. Puntik Kemuning No.88, Peanut, Tj. Karang Timur",
  "rating": 4.7,
  "review_count": 464,
  "phone": "0812-7923-7715",
  "phoneInternational": "+62 812-7923-7715",
  "phoneDigits": "081279237715",
  "website": "",
  "websiteKind": "none",
  "hasPhone": true,
  "hasWebsite": false,
  "contactable": true,
  "gapScore": 93,
  "gapTier": "hot",
  "isGapLead": true,
  "gapReason": "Tidak punya website sama sekali. Reputasi sudah bagus (rating 4.7, 464 ulasan). Ada nomor telepon publik sehingga bisa langsung dihubungi.",
  "suggestedOffer": "Website + Google Business Profile + landing page promo - tawarkan social proof dari ulasan yang sudah banyak"
}
```

### Contoh menjalankan

```bash
apify run umkm-google-maps \
  --input '{"searchStrings":["warung kopi","laundry"],"location":"Bandar Lampung","maxItems":40}'

## Kota lain
apify run umkm-google-maps \
  --input '{"searchStrings":["kopi"],"location":"Jakarta","maxItems":100}'

## Kota luar negeri: sesuaikan countryCode, kalau tidak kolom Ulasannya
## mengikuti bahasa Indonesia
apify run umkm-google-maps \
  --input '{"searchStrings":["coffee"],"location":"Tokyo","countryCode":"jp","language":"en"}'
```

### Proxy (opsional)

Kalau kena rate limit, actor bisa scraping lewat proxy. Kredensialnya **tidak
pernah ada di input schema** — hanya lewat environment variable `SCRAPER_PROXY`
dengan format `http://user:pass@host:port`.

Untuk run di Apify, atur lewat **Console → actor ini → Environment
variables**, bukan lewat `actor.json` dan bukan lewat input run. Isi
`actor.json` hanya dipakai saat deploy pakai Apify CLI; deploy dari GitHub
mengambil nilainya dari Console.

Untuk run lokal, set saja di shell:

```bash
export SCRAPER_PROXY="http://user:pass@host:port"
```

Kalau kosong, actor berjalan tanpa proxy.

### Catatan teknis

Scraping memakai `curl_cffi` untuk memalsukan sidik jari TLS seperti Chrome.
Tanpa itu, Google memblokir request di level jaringan. Rate limiting 1,5-3
detik plus exponential backoff sudah menyatu di dalam kode dan tidak bisa
dimatikan.

Struktur indeks field Google ada di
`packages/gmaps-core/src/gmaps_core/indexes.py` — satu-satunya tempat yang
perlu diubah kalau Google mengubah responsnya.

### Batasan

- Maksimal 40 hasil per request (batas Google), jadi `maxItems` dipecah
  otomatis. Batas keras `maxItems` = 480 per kata kunci. Kalau hasilnya
  kurang dari `maxItems`, berarti memang tidak ada tempat lagi di area itu —
  bukan scraping-nya gagal.
- `location` harus berupa **nama kota yang ada di tabel**. Alamat jalan,
  nama formatted, atau nama kelurahan (`Kemang`, `Kemang, Jatiasih`) tidak
  dikenali. Pakai `lat`/`lng` untuk kasus seperti itu.
- Nomor telepon hanya ada kalau pemilik usaha menempelkannya secara publik
  di profil Google Business. Actor tidak menebak atau mengarang nomor.
- Data berasal dari Google Maps. Patuhi ketentuan layanan Google, dan jangan
  memakai actor ini untuk mengumpulkan data pribadi yang tidak dipublikasikan
  pemilik usaha.

# Actor input Schema

## `searchStrings` (type: `array`):

Satu atau beberapa kata kunci. Contoh: 'Kopi', 'laundry', 'bengkel motor'. Actor akan menggabungkan tiap kata kunci dengan nilai Lokasi di bawah.

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

Nama kota yang jadi titik tengah pemindaian, contoh 'Bandar Lampung' atau 'Jakarta'. Dicocokkan ke tabel kota offline (Indonesia + kota besar dunia, ~800 nama), jadi tidak perlu mencari koordinat sendiri. Tulis 'Kota, Negara' kalau nama-nya bentrok, contoh 'San Jose, United States'. Nama yang tidak ada di tabel akan ditolak, bukan ditebak diam-diam. Kalau diisi Latitude dan Longitude, keduanya mengabaikan nilai ini.

## `lat` (type: `number`):

Kosongkan kecuali sudah tahu persis koordinat yang diinginkan. Kalau diisi, wajib berpasangan dengan Longitude, dan nilai Lokasi diabaikan sebagai pusat.

## `lng` (type: `number`):

Kosongkan kecuali sudah tahu persis koordinat yang diinginkan. Kalau diisi, wajib berpasangan dengan Latitude. Contoh Jakarta: lat -6.2088, lng 106.8456.

## `radiusKm` (type: `number`):

Radius dari titik tengah. Makin besar = makin banyak hasil, makin lambat.

## `maxItems` (type: `integer`):

Batas jumlah hasil yang dikembalikan per kata kunci.

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

Kode bahasa respons Google, contoh 'id' atau 'en'.

## `countryCode` (type: `string`):

Kode negara respons Google, contoh 'id' atau 'us'.

## `onlyGapLeads` (type: `boolean`):

Jika true, hanya hasil yang tidak punya website sendiri DAN punya nomor telepon yang dikembalikan. Ini yang Anda butuhkan untuk lead generation.

## `minGapScore` (type: `integer`):

Buang lead dengan skor di bawah nilai ini (0-100).

## `includeUnproven` (type: `boolean`):

Jika true, bisnis yang belum punya ulasan Google ikut disertakan. Default false karena belum terbukti.

## `includeRaw` (type: `boolean`):

Jika true, semua tempat dikembalikan beserta penanda gapScore, sehingga Anda bisa menyaring sendiri di luar actor.

## `debugDump` (type: `boolean`):

Simpan payload Google apa adanya ke storage. Nyalakan hanya kalau hasil terlihat aneh.

## Actor input object example

```json
{
  "searchStrings": [
    "Kopi"
  ],
  "location": "Bandar Lampung",
  "radiusKm": 30,
  "maxItems": 20,
  "language": "id",
  "countryCode": "id",
  "onlyGapLeads": true,
  "minGapScore": 0,
  "includeUnproven": false,
  "includeRaw": false,
  "debugDump": false
}
```

# Actor output Schema

## `leads` (type: `string`):

Semua hasil pindai UMKM di Google Maps, termasuk penilaian celah digital, alasan, dan saran penawaran.

# 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 = {
    "searchStrings": [
        "Kopi"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("syzen/apify").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 = { "searchStrings": ["Kopi"] }

# Run the Actor and wait for it to finish
run = client.actor("syzen/apify").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 '{
  "searchStrings": [
    "Kopi"
  ]
}' |
apify call syzen/apify --silent --output-dataset

```

## MCP server setup

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

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/ey2Odeuc3dOfBslzb/builds/T7oXvETYtpsnXqfAj/openapi.json
