--- name: openstreetmap-places-scraper description: Geocode, reverse-geocode and harvest points of interest from OpenStreetMap via the Apify Actor arman-bd/openstreetmap-places-scraper. Returns one record per place with coordinates, the full display name, broken-out address components, the OSM object type, id and link, classification pair, ranking signals and the raw mapper tags such as opening hours, phone and website. Use when a task needs bulk address geocoding, address validation, coordinates turned back into streets, or every shop or amenity of a given kind inside a bounding box. Not for driving directions, live traffic, business reviews or ratings. --- # OpenStreetMap Places Scraper Apify Actor `arman-bd/openstreetmap-places-scraper`. Three jobs in one Actor: forward-geocode addresses, reverse-geocode coordinates, and sweep an area for every object carrying a tag. All three produce the same record shape, so a mixed run gives you one uniform table. The Actor takes no credentials. ## When to use it - Bulk address geocoding: a customer list in, coordinates and standardised components out. - Address validation, comparing what you hold against `displayName` and the broken-out components. - Store-locator and competitor mapping: sweep a city box for `shop=supermarket` or `amenity=fuel`. - Turning GPS telemetry into street, city and postcode. - Enriching a places database with opening hours, phone and website from the raw tags. ## When not to use it - Routing, directions, distance matrices or live traffic. None are returned. - Reviews, ratings, photos or popularity. The map has no such data. - Guaranteed coverage of commercial premises. Everything here is volunteer-mapped, so completeness varies enormously by country and category. - Geocoding tens of thousands of addresses quickly. Forward and reverse lookups are paced at one per second by policy, so 10000 addresses is close to three hours. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/openstreetmap-places-scraper').call({ poiTags: ['amenity=cafe', 'shop=bakery'], areaBoundingBox: { south: 52.5, west: 13.36, north: 52.53, east: 13.42 }, includeExtraTags: true, maxResults: 2000, contactEmail: 'you@example.com', }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~openstreetmap-places-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"queries":["Brandenburg Gate, Berlin"],"includeAddressDetails":true,"contactEmail":"you@example.com"}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/openstreetmap-places-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input At least one of `queries`, `coordinates`, or `poiTags` with `areaBoundingBox` is required. All three can run together. | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `queries` | string[] | no | `[]` | Free-text addresses or place names to forward-geocode. Anything a map search box accepts. | | `coordinates` | string[] | no | `[]` | `"lat,lon"` strings to reverse-geocode, for example `"52.5163,13.3777"`. Comma, semicolon or space separated. An unparseable or out-of-range pair aborts the whole run rather than silently dropping a row. | | `poiTags` | string[] | no | `[]` | Tag filters such as `amenity=cafe`, or a bare key such as `amenity` to match any value. Aborts the run if set without `areaBoundingBox`. | | `areaBoundingBox` | object | no | none | `{ south, west, north, east }` in WGS84 degrees. Read only when `poiTags` is set. An inverted or out-of-range box aborts the run. | | `countryCodes` | string[] | no | `[]` | Two-letter ISO codes confining forward geocoding. Lower-cased for you. Ignored by reverse and POI lookups. | | `includeAddressDetails` | boolean | no | `true` | Break out `houseNumber`, `road`, `city`, `state`, `postcode`, `country`. Off, those six are `null`. `countryCode` is unaffected. | | `includeExtraTags` | boolean | no | `false` | Attach raw mapper tags to geocoding rows. POI rows always carry them regardless. | | `resultsPerQuery` | integer | no | `1` | Candidate matches kept per forward-geocoding query. Clamped to 1 to 40. | | `maxResults` | integer | no | `1000` | Total places saved across the whole run, all modes combined. Also becomes the limit on the area sweep. Maximum 50000. | | `contactEmail` | string | no | `""` | Goes into the identifying header sent with every request. Set it for anything scheduled: the usage policy asks for a way to reach whoever is running the queries. The Actor logs a warning when it is missing. | **The two halves of this Actor have opposite cost shapes.** Geocoding is paced at one request per second and each address is its own request, so `queries` and `coordinates` turn directly into wall-clock time: budget a second per entry, plus a second per failure. An area sweep is a single query no matter how many tags you pass, so its risk is not time but size: a large box with a common tag is a lot of geometry and can be refused outright. Keep the box tight and split a city into quadrants run as separate Actor runs rather than widening one box. ## Output One record per place, identical in shape whichever mode produced it. Rows are de-duplicated across the run on the OSM object, so a place found by both a geocode and a sweep is saved once. | Field | Type | Notes | |---|---|---| | `query` | string | The input that produced this row: the address, the coordinate pair, or the joined tag list. | | `queryType` | string | `search`, `reverse` or `poi`. | | `source` | string | Which service answered. `nominatim` for geocoding rows, `overpass` for sweep rows. Several fields depend on this, see below. | | `placeId` | number \| null | Internal geocoder id. Always `null` on sweep rows. | | `name` | string \| null | Object name. Frequently `null` on sweep rows: plenty of mapped objects are unnamed. | | `displayName` | string \| null | Full human-readable label. On sweep rows the Actor assembles it from the address tags, so it is shorter and can be `null`. | | `latitude` | number \| null | WGS84 decimal degrees. | | `longitude` | number \| null | | | `osmType` | string \| null | `node`, `way` or `relation`. | | `osmId` | number \| null | | | `osmUrl` | string \| null | Direct link to the object on the map site. | | `class` | string \| null | Classification key, for example `amenity`. On sweep rows it is the first of your `poiTags` keys the object carries. | | `type` | string \| null | Classification value, for example `cafe`. | | `addressType` | string \| null | Geocoder ranking signal. `null` on sweep rows. | | `placeRank` | number \| null | Geocoder ranking signal, lower is broader. `null` on sweep rows. | | `importance` | number \| null | Geocoder relevance score. Useful for choosing between candidates. `null` on sweep rows. | | `boundingBox` | object \| null | `{ south, north, west, east }` extent. Always `null` on sweep rows. | | `houseNumber` | string \| null | `null` when `includeAddressDetails` is off. | | `road` | string \| null | As above. | | `city` | string \| null | As above. Collapsed from city, town, village or municipality on geocoding rows. | | `state` | string \| null | As above. | | `postcode` | string \| null | As above. | | `country` | string \| null | As above. In the local language. | | `countryCode` | string \| null | Lower-case two-letter code. Not gated by `includeAddressDetails`. | | `extraTags` | object \| null | Raw mapper tags: `opening_hours`, `website`, `phone`, `wheelchair`, `brand`, `cuisine` and whatever else was recorded. `null` on geocoding rows when `includeExtraTags` is off. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real geocoding record: ```json { "query": "Brandenburg Gate, Berlin", "queryType": "search", "source": "nominatim", "placeId": 143634177, "name": "Brandenburger Tor", "displayName": "Brandenburger Tor, 1, Pariser Platz, Friedrich-Wilhelm-Stadt, Mitte, Berlin, 10117, Deutschland", "latitude": 52.5162699, "longitude": 13.3777034, "osmType": "way", "osmId": 518071791, "osmUrl": "https://www.openstreetmap.org/way/518071791", "class": "building", "type": "gate", "addressType": "building", "placeRank": 30, "importance": 0.5504719991549523, "boundingBox": { "south": 52.516117, "north": 52.5164328, "west": 13.3775798, "east": 13.3778251 }, "houseNumber": "1", "road": "Pariser Platz", "city": "Berlin", "state": null, "postcode": "10117", "country": "Deutschland", "countryCode": "de", "extraTags": { "height": "26", "tourism": "attraction", "historic": "monument", "wheelchair": "yes", "opening_hours": "24/7", "wikidata": "Q82425" }, "scrapedAt": "2026-08-06T12:00:00.000Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it**, and read `targetsRequested` carefully: it counts targets, not places. ```json { "targetsRequested": 3, "targetsFailed": 0, "failures": [], "placesSaved": 412, "filters": { "queries": ["Brandenburg Gate, Berlin"], "coordinates": ["52.5163,13.3777"], "poiTags": ["amenity=cafe", "shop=bakery"], "areaBoundingBox": { "south": 52.5, "west": 13.36, "north": 52.53, "east": 13.42 }, "countryCodes": [], "includeAddressDetails": true, "includeExtraTags": true, "resultsPerQuery": 1, "maxResults": 2000 }, "finishedAt": "2026-08-06T12:00:07.221Z" } ``` A target is one address, one coordinate pair, or the entire area sweep however many tags it carries. `targetsFailed` above zero means the run is partial, with one `{ target, type, error }` per failure. `placesSaved` equal to `maxResults` means the cap bound and later targets may have been skipped entirely, since the cap is checked before each target and again before each row. `contactEmail` is deliberately absent from `filters`. ## Behaviour to plan around - **All `poiTags` become one query, not one per tag.** They are combined into a single union over the same box, and the sweep counts as a single target. You cannot tell from the summary which tag contributed how much; group the dataset by `class` and `type` for that. - **On a sweep row, `class` is the first of your tags that matched.** An object carrying both `amenity=cafe` and `shop=bakery` is reported under whichever appears first in your `poiTags`, and appears once. Order your tags by which classification you would rather see. - **Six fields are geocoding-only.** `placeId`, `addressType`, `placeRank`, `importance` and `boundingBox` are always `null` on sweep rows, and `displayName` is assembled rather than authoritative. Branch on `source` before ranking candidates. - **Ways and relations get a centroid, not a shape.** The sweep asks for centre points, so `latitude` and `longitude` on a building or a park are its centre and `boundingBox` is `null`. Do not treat a centroid as an entrance or an address point. - **Names come back in the local language.** `Deutschland`, not `Germany`, and the same for cities and streets. `countryCode` is the stable field to key on. - **`includeAddressDetails: false` nulls six fields but not `countryCode`.** If all you need is the country, you can leave the flag off. - **`extraTags` behaves differently per mode.** On sweep rows it is always the full raw tag object. On geocoding rows it is `null` unless `includeExtraTags` is on. It is never a partial subset, and its keys are mapper-chosen, so treat every key as optional. - **A bad input aborts before any request, a bad response does not abort at all.** Malformed coordinates, tags without a box, or an inverted box throw immediately. A target that fails mid-run lands in `failures` and the rest continue; the Actor only throws when every target failed. - **Retries are three attempts with linear backoff.** A refused or malformed query is final and is not retried. A first-attempt timeout on a large area sweep is ordinary and usually succeeds on the retry. - **`resultsPerQuery` above 40 is clamped.** Forty candidates per address is the ceiling, and 1 is the right value when you are standardising a list of addresses you already believe in. ## Recipes **Standardise a customer address list.** One best match per address, components broken out. ```json { "queries": ["10 Downing Street, London", "Eiffel Tower, Paris"], "countryCodes": ["gb", "fr"], "includeAddressDetails": true, "resultsPerQuery": 1, "contactEmail": "you@example.com" } ``` Join back on `query`, then compare `postcode` and `road` against what you hold. Rows with a low `importance` are the ones worth a human look. **Sweep an area for competitors.** One query, tight box, tags on. ```json { "poiTags": ["shop=supermarket", "shop=convenience"], "areaBoundingBox": { "south": 52.5, "west": 13.36, "north": 52.53, "east": 13.42 }, "includeExtraTags": true, "maxResults": 2000, "contactEmail": "you@example.com" } ``` Group by `type` for the split between the two shop kinds, and read `brand` and `opening_hours` out of `extraTags`. If the run comes back empty, the tag is usually wrong before the box is. **Reverse-geocode a GPS trace.** One second per point, so keep batches modest. ```json { "coordinates": ["48.8584,2.2945", "52.5163,13.3777", "51.5007,-0.1246"], "includeAddressDetails": true, "contactEmail": "you@example.com" } ``` `road`, `city` and `postcode` are the fields worth keeping; `displayName` is long and duplicates them. **Pick between ambiguous place names.** Ask for candidates instead of one answer. ```json { "queries": ["Springfield"], "resultsPerQuery": 20, "includeAddressDetails": true, "contactEmail": "you@example.com" } ``` Sort the 20 rows by `importance` descending and disambiguate on `state` and `countryCode`.