Scrap (Google Maps)
Source module that extracts Google Maps listings for a set of search queries across one or more geographic zones, in any of ~199 countries.
Purpose
Source module: runs each query over every grid point covering the requested zones and returns a flat CSV of Google Maps establishments (name, phone, address, website, rating and review count).
Inputs
| Field | Type | Required | Description |
|---|---|---|---|
queries |
string[] (1–20) |
yes | Search terms run against Google Maps. Each query is trimmed and capped at 200 chars. |
country |
string (ISO3) |
no | Target country as an ISO-3166 alpha-3 code (e.g. FRA, USA, DEU). Defaults to "FRA". Determines both the GPS grid and how zones are resolved. |
zones |
string[] (1–50) |
yes | Geographic zones expressed within country: a state/region (adm1), a county/department (adm2), a city with an optional radius ("Austin 30km"), or the country name itself to scrape the whole country. For France, INSEE codes, department codes and postal codes remain valid. Each zone is resolved to a grid of points server-side. City and region names are accepted in the usual languages ("Bruxelles", "Brussel" and "Brussels" resolve to the same place), and associated or delegated French communes ("Lomme") resolve to their own location. A name shared by several cities means the most populous one; a name shared by a large city and the area around it means the city ("Antwerp", "New York"), and the whole area is requested by its title ("New York State"). Corsica is written "2A", "2B" or "Corse". The overseas departments (Guadeloupe, Martinique, Guyane, La Réunion, Mayotte) are requested by name or code ("Martinique", "974"), or by one of their communes or postal codes; "France" alone does not include them. A commune there that shares its name with a mainland one is written with its department ("Saint-Denis 974", "Saint-Denis La Réunion") or its postal code ("97400"). |
include_reviews |
bool |
no | Kept for backward compatibility. Does not chain a reviews job: use the reviews module instead. Defaults to false. |
max_per_phone |
int (0–10) |
no | Maximum listings kept per phone number. 0 (default) = no limit; 1 = a single contact per number. Listings with no number are never dropped. The same listing never appears twice regardless. See Duplicates. |
exclude_foreign_phones |
bool |
no | true (default): drops listings whose phone number carries another country's calling code. Numbers without a country code and listings with no number are kept. See Foreign phone numbers. |
Request body:
{
"queries": ["plombier", "chauffagiste"],
"country": "FRA",
"zones": ["75", "92"],
"include_reviews": false
}
Targeting another country instead; zones are expressed in that country:
{
"queries": ["plumber", "hvac"],
"country": "USA",
"zones": ["Texas", "Austin 30km"]
}
Effective Google Maps requests = len(queries) × grid_points(zones). Rejected at submit if cost exceeds the per-job EF ceiling.
Outputs
Result file: UTF-8 CSV, semicolon delimiter, BOM (Excel-safe). The same dataset is available in three formats through the download endpoint.
| Column | Type | Description |
|---|---|---|
nom |
string | Establishment name as displayed on Google Maps. |
telephone |
string | Phone number as listed. |
adresse |
string | Street address (number and street) as shown in the Maps list. |
ville |
string | City, taken from Google's own structured place data (exact, including Paris, Lyon and Marseille arrondissements and multi-postcode cities). Empty for listings Google has no address for. |
code_postal |
string | Postal code, from the same Google source as ville. |
site_web |
string | Public website URL, when listed. |
lien_google_maps |
string | Google Maps URL of the listing. |
note |
number | Average star rating, from 0 to 5. Empty when the listing has none. |
nb_avis |
integer | Number of public reviews. Empty when unknown. |
query |
string | Query that produced the row. |
date_scraping |
string | Date and time the row was collected. |
villeandcode_postalcome from the structured place data Google ships with each result, not from reverse geocoding, so they match Google exactly. The Maps list view only renders the street, which is whyadressealone never carries the city.
The file has no category, opening-hours or directory-flag column. Websites that are really a directory listing rather than the business's own site are recognised by dead_check (status = aggregator) and can be dropped with the exclude_aggregators rule of filter.
Optional columns
Three extra columns are off by default and enabled per job via extra_columns (a list). The default output is street + ville + code_postal.
Option in extra_columns |
Adds column(s) | Description |
|---|---|---|
gps |
lat, lon |
Exact latitude/longitude of the business (Google's own coordinates, not a grid approximation). |
departement |
departement |
Department/county name (adm2). In France, derived from code_postal; in any other country, derived from the business GPS via world reverse-geocoding. |
region |
region |
Region/state name (adm1). In France, derived from code_postal; in any other country, derived from the business GPS via world reverse-geocoding. |
Example request body: { "queries": ["plumber"], "zones": ["Paris 10km"], "extra_columns": ["gps", "departement", "region"] }.
Foreign phone numbers
A search area that touches a border also returns businesses from the other side: a scrape of Lille (France) brings back salons in Tournai (Belgium), with numbers in +32.
exclude_foreign_phones, on by default. A listing is dropped when its phone number explicitly carries the calling code of another country than the one you selected. Everything else is kept:
| Listing | Result |
|---|---|
Number written without a country code (03 20 53 00 86, 06 26 55 99 69) |
Kept: that is how numbers of the selected country are written. |
| Number with the selected country's calling code | Kept. |
Number with another country's calling code (+32 69 23 26 64 in a France scrape) |
Dropped. |
| No phone number | Kept. |
| Unreadable number or unknown calling code | Kept. |
It works the same way in every country. Calling codes shared by several countries (+1 for the United States, Canada and the Caribbean, +7 for Russia and Kazakhstan, +44 for the United Kingdom and the Channel Islands) are resolved from the number range, not from the code alone. Numbers from a country's overseas territories count as that country's (Guadeloupe or Réunion for France, Puerto Rico for the United States).
Dropped listings do not count toward stop_at, and the job log ends with the number of listings dropped this way. Send "exclude_foreign_phones": false (or untick Exclude foreign phone numbers in the advanced settings) to keep them.
Duplicates
One listing = one row. Always, with no setting to turn it off. A business can sit within reach of several GPS grid points and match several of your queries, and Google's listing URL is not a stable identity: it also encodes the search context, so two captures of the same place come back with different URLs. Rows are therefore deduplicated on the 0x…:0x… feature id Google carries inside that URL (falling back to normalised name + address when it is absent). This is a correctness guarantee, not a preference.
max_per_phone: the one thing you can tune. Different listings sometimes share a phone number: a shared switchboard, an owner running several outlets, or a network of near-identical local listings pointing at one call centre. For a chain those are genuinely distinct establishments; for prospecting they are one contact. That trade-off is yours to make:
| Value | Behaviour |
|---|---|
0 (default) |
No limit: every listing is kept, whatever number it shows. |
1 |
One listing per phone number; the first one found wins. |
2, 3, … (max 10) |
Up to N listings per number. |
Numbers are compared in E.164 form, so +33 1 87 58 88 84 and 01 87 58 88 84 are the same number; the job's country sets the region used to parse national formats. Listings with no usable number are never dropped by this setting.
The quota applies across the whole job, queries and zones included, and is preserved when a cancelled job is resumed. Deduplication never changes which grid points get scraped, so it has no effect on EF cost or runtime.
Formats: csv (original), json, xlsx. Selected via ?format= on the download endpoint.
Lifecycle
Standard job lifecycle: see Jobs & lifecycle. While running, the SSE status event carries a query_stats payload of shape { "<query>": { "tiles": int, "with_results": int } }, updated in real time to expose per-query hit ratio.
Pipeline
| Field | Value |
|---|---|
needs |
null (source module: no input CSV required) |
produces |
poi_list |
Typical downstream modules chained against a scrap output:
emails: find professional and personal emails fromsite_web.socials: extract social network handles fromsite_web.legal_ids: extract SIREN/SIRET from the establishment's website (legal-mentions page).reviews: collect full review threads fromlien_google_maps.techstack,dead_check,brand_assets,ads_intelligence: site-level enrichments keyed onsite_web.
Endpoints
Dedicated endpoint:
POST /api/jobs
Content-Type: application/json
{
"queries": ["plombier"],
"zones": ["75"],
"include_reviews": false
}
Generic job endpoint (equivalent; same payload, job_type inferred from shape):
POST /api/jobs
Content-Type: application/json
{
"job_type": "scrap",
"queries": ["plombier"],
"zones": ["75"]
}
Both responses return the created JobPublic object including id, status, grid_points_count, ef_cost and output_filename.
Download:
GET /api/jobs/{job_id}/download?format=csv|json|xlsx
Limits
Platform-wide quotas: see /docs/concepts/limits. Module-specific caps:
| Limit | Value |
|---|---|
| Maximum queries per job | 20 |
| Maximum zones per job | 50 |
| Maximum query length | 200 chars |
| Maximum cost per job | 5 EF by default (per user, adjustable up to 50) |
| Email verification | Required on the account before a scrap job can be created. |
Errors
| Scenario | HTTP | Resolution |
|---|---|---|
| Unrecognised zone string | 400 | Inspect the errors array in the response body. Check the zone is spelled as it exists in the selected country (a state/region, county/department, "City 30km", or the country name). For France, INSEE/department/postal codes and "France" also work. When a close match exists, the error names it (suggestion). |
| No grid points resolved | 400 | The zone set is empty after resolution: broaden the zone selection. |
| EF quota exceeded | 400 | Reduce the number of queries or shrink the zones until estimated EF is within your per-job ceiling (5 by default, adjustable up to 50 in Settings). |
| Email not verified | 403 | Verify the account email before creating a scrap job. |
| No worker available | The job stays in pending until the shared multi-proxy pool is free. Only one multi-proxy job runs at a time platform-wide. |
|
| Job failed mid-run | A partial CSV is preserved. A POST /api/jobs/{id}/resume creates a follow-up job that skips already-processed grid points and is billed only for the remainder. |
|
| Maximum duration reached | A job runs for at most 24 hours. It then stops with its partial CSV preserved and a follow-up job is queued automatically for the remaining grid points, billed only for the remainder. Nothing to do: follow the new job, linked by source_job_id. |
|
| Download expired | 410 | Result files have a retention window: re-run the job or chain from a fresh source. |
Queries refused by Google Maps surface in dead_queries on the job object.
Data sources & attribution
Worldwide zone resolution and reverse-geocoding are built on open datasets:
Worldwide administrative boundaries © geoBoundaries (CC BY 4.0) · Cities © GeoNames (CC BY 4.0).