# Changelog of Google Maps · Emails, Phones & Socials by Search (`corent1robert/google-maps-search-scraper`) Actor

- **URL**: https://apify.com/corent1robert/google-maps-search-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/corent1robert/google-maps-search-scraper.md

## Changelog

All notable changes to this Actor will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### \[1.9] - 2026-09-07

#### Added

- **Actor Standby:** HTTP lookup `GET`/`POST /lookup` — **one** search phrase, **one** place row. Website enrichment is skipped. Console **Start** still writes the dataset as before (bulk queries + optional website crawl).
- OpenAPI spec for the Standby tab (`web_server_openapi.json`).

### \[1.8.1] - 2026-08-28

#### Changed

- Console and README point trade + city discovery to **Google Maps · Leads by Category & City**. This Actor stays name + address pin match.

### \[1.8] - 2026-08-25

Apify actor version: **1.8**.

#### Added

- **Published emails from the business website** (`enrichFromWebsite`, default **on**): homepage + contact / legal pages. Never invented. Dataset fields `email` / `emailSource`.
- **Pay-per-event:** `place-with-email` (primary, **$1.50 / 1k** on Free, socials included), `place-with-phone` (**$0.50 / 1k**), `place-without-phone` (**$0.20 / 1k**). No contact / social add-on.
- Dataset views **Overview** + **Outreach / CRM**, **12** unpublished task specs in `published-tasks/`.

#### Changed

- Console is two fields: **name + address** (string list, one row per business) and **find email on the website**. Volume = list × **3** places. **`maxPlaces` is API-only** (0 = no ceiling). **Free plan: 20 / run.**
- Maps country is inferred from the phrases (France if unclear). Each result is opened by default. Default **8** searches at once.
- Store listing, README (personas, fill rates, pricing tiers, legality), and run logs rewritten for buyers — outcome wording, truncated phrases, no tab-crash dumps in default logs.
- **PPE lowered** to stay under Maps incumbents: **$1.50 / 1k** with email (socials included), **$0.50 / 1k** with phone, **$0.20 / 1k** without. No contact or social add-on.
- Website email lookup for the up-to-**3** places in a search runs **in parallel** (homepage, then contact/legal pages together when more contacts are still missing).
- Website crawl also keeps a **published phone** when Maps has none, and public **social profile URLs** (Facebook, Instagram, LinkedIn, TikTok, YouTube, X, WhatsApp). Never invented.
- Console default is **name + street + postcode + city** (pin matching / CRM enrich), not a trade + city search. Trade + city still works for prospecting (up to 3 places).
- **Speed:** Facebook contacts run **once after Maps** (no mid-run nested Actor that blocked a Maps tab every 20–40 places). Paid nested batches are **500** pages. Website HTTP timeout **8s**. Site crawls after Facebook run **8** at a time.
- README output sample is the real Console default pin (published Outlook mailbox via Facebook, Planity site, Instagram, **28** reviews). Proxy claim removed (this Actor does not use Apify Proxy). Nested Facebook **Full permissions** and local `--input-file` warning documented.

#### Fixed

- **`reviewCount`** on name + address matches: the star rating paints before `(28)` / `28 avis` in `.F7nice`. The Actor waits for that count (and re-reads if a rating is present with an empty count). Maps `hl` cookie is set on **google.fr** as well as google.com so cloud runs stay in the inferred language.
- Address no longer keeps a glued French **Fermé** / **Ouvert** suffix (`GuilbaudFermé` → `Guilbaud`). Word-boundary `\b` failed after `é`.
- Facebook / Instagram / LinkedIn / TikTok / YouTube / X URLs are not stored as **website**. A Facebook URL still fills **`facebookUrl`**. Host matching is exact (`box.com` is not treated as `x.com`).
- Name + address matches no longer keep a **/maps/search/** URL as `mapsUrl` when a **/maps/place/** permalink or coordinates exist. `latitude` / `longitude` follow that URL. `phoneSource` and empty social keys are always written, even when Maps has no website (so no email crawl).
- When Maps has **no crawlable website** but a **Facebook page**, the run calls [`corent1robert/facebook-page-contact-scraper`](https://apify.com/corent1robert/facebook-page-contact-scraper) for a **published** mailbox (`emailSource`: `facebook-page`). One nested run after Maps on Free (≤20 places); paid runs chunk **500**. Nested Actor events are billed separately. Never invented.

### \[1.6] - 2026-04-09

#### Changed

- **`maxMemoryMbytes`:** **16 GB → 24 GB** (`16384` → `24576`) in `actor.json` so heavy runs with high **`searchConcurrency`** can allocate more RAM on Apify.
- **List-only extraction (faster):** for multi-result searches the Actor **no longer opens each place** in the detail panel — it reads **only the sidebar list** (name, Maps link, rating/reviews when shown, category/address lines when present). **Phone, website, plus code, opening line, Facebook, etc. are usually empty** on list rows. Single-result / detail-first layout uses **one** `placeEvaluate` on the current view **without** long review-panel waits. **No proactive tab recycle** between searches — **one tab per worker** for the whole run (new tab only on **crash recovery**).
- **Console input (Shopify-style simplicity):** only **Maps search phrases**. **Places per phrase** removed from the form; **hard cap 3** per phrase in `main.js` (`maxPlacesPerSearch` in API/JSON is clamped to **1–3**, so old saved inputs like **5** no longer apply). No **proxy**, **speed mode**, or **parallel tabs** in the form — defaults are **fast timing**, **8 parallel tabs**, and **`fast` request trimming** (images, media, fonts, text tracks, and common analytics/ad URLs via `fastRequestBlock.js`), fixed in `main.js`. API/JSON runs can still pass `speedMode` or `searchConcurrency` if needed.
- **Copy:** shorter top-level description, two sections (**What to scrape** / **Connection**), less jargon in logs (no tab-count lecture on startup).
- **Speed (large runs):** `speedMode: fast` waits tightened again in `mapsScrapeTiming.js`; feed scroll targets **`maxPlaces + 2`** cards (was +5); nav-retry backoffs slightly shorter in `mapsPuppeteer.js`. **Removed per-search `setStatusMessage` on start** — only updates after each finished search (avoids thousands of platform API calls on 1000+ queries).
- **`setStatusMessage` throttled** (~every **12** completions or **20s**) on huge runs.
- **Resilience:** **`Page crashed`** / **detached Frame** treated as recoverable (`mapsPuppeteer.js`). Each worker **retries the same search up to 3 times** with a **fresh `Page`** before logging a skip and continuing (run no longer dies mid-dataset). **`isRecoverableBrowserError`** exported for tests.
- **`searchConcurrency`:** maximum raised from **8** to **32** via API/JSON; default stays **8**. High values need **more Actor memory** and may increase Maps throttling.

#### Fixed

- **List row field mix-ups:** sidebar blobs (category · address · hours · phone on one or more lines) are parsed in Node via **`parseListCardBlocks`**: French phones extracted, opening summary split off, compact ratings like **`4,2 (158)`** read from text, private-use icon glyphs stripped, duplicate lines deduped, glued words repaired (**`LauragaisOpen` → `Lauragais Open`**). **`aria-label`** fills **`reviewCount`** when parentheses are missing. **`website`**: `business.google.com/create` and Maps links ignored. **`permanentlyClosed`** also derived from “Permanently closed” / “Définitivement fermé” in the opening summary.
- **Hours vs address:** fixed **`Opens?` / `Closes?`** in the opening regex (they matched **`Open` inside `Opens`** and **`Close` inside `Closes`**, leaving garbage like **`Open s 7 PM`** on the address). Replaced with explicit **`Opens|Open`** and **`Closes|Close`** (longer tokens first). Added **`stripStrayOpeningTailFromAddress`** as a safety net. List cards: broader **`reviewCount`** plus Node fallback on **`aria` + `listTextLines`**.
- **Hours `Open 6:30 PM`:** **`OPENING_SPACE_TIME_RE`** peels comma- or dot-separated clock times without a middot before the hour (EN UI). **`stripStrayOpeningTailFromAddress`** also strips **`Open 6:30 PM`** tails; repair when **`openingSummary`** was only **`Closes`** but the address still had **`Open …`**. **`reviewCount`:** compact **`4,5 (N)`**, **`N avis` / `N reviews`**, rating-hint anchored match; stricter **`parseGoogleMapsReviewCountTextStrict`** prefers **`\d[.,]\d+(N)`** before generic parens.

### \[1.5] - 2026-04-09

#### Added

- **Fast timing** in code (`speedMode` **fast** by default): shorter waits; **images, media, fonts, text tracks, and known tracking/ad requests** blocked per tab unless `speedMode: balanced` is passed via API.

- **`mapsScrapeTiming.js`**: central timing table (unit-tested).

- **Parallel searches** in code (default **8** tabs, max **8**): several phrases at once; each tab opens places **one at a time** (Maps UI).

- **Run progress in logs**: after each search, a line with **fraction**, **percent**, ASCII **progress bar**, places added this round, total saved, and **ETA** (~remaining time once at least two searches have finished). **Status message** on Apify shows the same summary for the run header.

- **`latitude` / `longitude`** on each dataset row when the place **mapsUrl** contains `@lat,lng` or `!3d…!4d…` (best-effort parse in `mapsPure.js`).

#### Fixed

- **Extra blank tab:** after opening worker tabs, the default **about:blank** page is closed so you do not get one more Chromium target than `searchConcurrency`. Startup log clarifies **one browser**, **N tabs** = `searchConcurrency`, vs **max places per search** (cards opened in sequence in each tab).

- **`Actor.setStatusMessage`**: Apify SDK expects a **string** as the first argument and optional `{ level }` as the second — not `{ message, level }`. Fixes platform crash on long multi-search runs.

- Startup logs no longer print **every** search phrase when there are more than **30** (shows first 30 + count of the rest).

- **Address labels**: **`Address:`** and **`Adresse:`** (and spacing variants) are stripped in the browser and again in **`pushNormalized`** via **`stripMapsAddressLabelPrefix`**, so **`fullAddress`** / **`streetAddress`** no longer repeat the UI prefix.

- **Plus Code**: extraction uses an **Open Location Code–style** pattern so values like **`MHC8+RG La`** become **`MHC8+RG`** instead of junk locality text.

- **List vs detail log**: the line that opens each card now says it is **from the Maps results list** and notes that **direct single-place detail** (no list) **does not** use that step.

#### Changed

- Removed **`mapsBaseUrl`** and **`navigationTimeoutMs`** from Console input; Actor always uses **https://www.google.fr/maps** and a **120s** navigation budget (set in `main.js`).
- Removed `**verboseLogs**` from Console input schema (no troubleshooting toggle in the UI).
- **`actor.json` `description`** shortened to stay within Apify’s **300-character** Store limit (`apify push` rejects longer text). Support contact stays in the README.

#### Fixed

- **`key_value_store_schema.json`**: each `collections` entry must declare `"key": "RUN_LOG"` (Apify build validation); fixed failed deploy.

- `reviewCount` was often empty because the review line appears after the star rating; the Actor waits for that part of the panel before reading it, and prefers `aria-label` text such as `28 avis` inside `.F7nice`.

- The wait no longer looks only at the **first** `.F7nice` on the page (often a list row with stars only). It checks every `.F7nice` **outside** `[role="feed"]` so the detail column unlocks the wait (avoids list rows and avoids `[role="main"]` filter labels like “5 étoiles, 24 avis” that matched too early).

- Extraction prefers the same **non-feed** `.F7nice` order so the open place’s review line wins over list cards.

- If a **rating** is already visible but **review count** is still empty (slow SPA paint after `/place/` navigation), the Actor re-reads the panel several times before giving up.

- **Single-result / detail-first** layout waits a few seconds before reading the panel so client-side navigation from `/search` to `/place/` can finish (the previous check on `page.url()` ran too early).

### \[1.4] - 2026-04-09

#### Changed

- README rewritten for buyers: outcomes, audience, simple start steps, and plain-language caveats — no technical appendix.

### \[1.3] - 2026-04-09

#### Fixed

- `reviewCount` extraction: no longer depends on `getRating()` first; scans every `.F7nice` block, `[aria-label]` / `role="img"` with review wording, `h1` header ancestors (strict patterns to avoid photo counts), review links, and `4,9 (28)`-style text; normalizes narrow spaces. Slightly longer settle time after opening a place card before reading the panel.

### \[1.2] - 2026-04-09

#### Changed

- Run logs use `apify/log` (with RUN\_LOG mirroring) for censored, Store-friendly messages; progress wording focuses on outcomes, not internals.
- `setStatusMessage`: "Collecting places…".

#### Fixed

- Normalize `city` (strip trailing `, France` / `, FR`) and `plusCode` (trailing comma) in dataset rows.

#### Added

- README: sample output object and **Internals** note on why a browser session is required (plain HTTP does not return the same place payload).

### \[1.1] - 2026-04-09

#### Fixed

- `reviewCount` now resolves for more Google Maps layouts (full `.F7nice` block, following siblings, `aria-label`, and `N avis` / `N reviews` text).

### \[1.0] - 2026-04-09

#### Added

- Initial release: Google Maps text search to structured dataset rows (name, address, phone, category, rating, review count, website, Facebook link when visible, plus code, opening summary, Maps URL).
- Console input: `queries`, `maxPlacesPerSearch`. Maps host and page timeout are fixed in code (`google.fr`, 120s).
- `RUN_LOG` key-value record and dataset `overview` view.
- **Local runs:** always write `output.csv` (UTF-8 BOM, semicolon) at the project root, same columns as the dataset (header only if no rows).
