# OpenStreetMap Places Scraper: Geocoding & POI Search (`arman-bd/openstreetmap-places-scraper`) Actor

Geocode addresses, reverse-geocode coordinates and harvest points of interest from OpenStreetMap. Address components, bounding boxes and raw OSM tags. No key.

- **URL**: https://apify.com/arman-bd/openstreetmap-places-scraper.md
- **Developed by:** [Arman Hossain](https://apify.com/arman-bd) (community)
- **Categories:** Automation, Developer tools, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.74 / 1,000 place scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## OpenStreetMap Places Scraper: Geocoding & POI Search

![OpenStreetMap Places Scraper: Addresses geocoded, coordinates reverse-geocoded, and every point of interest inside an area you draw](https://api.apify.com/v2/key-value-stores/ZQOcNAOHrIgTacAmy/records/openstreetmap-places-scraper.jpg)

Does three things with the world's open map data: turns addresses into coordinates, turns coordinates back into addresses, and harvests every point of interest inside an area you draw.

It talks to the two OpenStreetMap endpoints built for exactly these jobs over plain HTTP: Nominatim for geocoding, Overpass for bulk area queries. There is no browser, no proxy, no credentials, no account and no per-request billing from a commercial geocoder. Rate limiting is built in, not left to you.

**Agent skill: [SKILL.md](https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/openstreetmap-places-scraper.md)**

```
https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/openstreetmap-places-scraper.md
```

### What you get

| Field | What it holds |
|---|---|
| `query`, `queryType` | The input that produced this row, and whether it was a `search`, a `reverse` or a `poi` lookup |
| `source` | `nominatim` or `overpass`, whichever endpoint answered |
| `name`, `displayName` | Object name, and the full human-readable label |
| `latitude`, `longitude` | WGS84 decimal degrees |
| `osmType`, `osmId`, `osmUrl` | The underlying node, way or relation and a link straight to it on openstreetmap.org |
| `class`, `type` | OSM classification pair, for example `amenity` / `cafe` |
| `addressType`, `placeRank`, `importance` | Nominatim's own ranking signals, useful for picking between candidate matches |
| `boundingBox` | `{ south, north, west, east }` extent of the object |
| `houseNumber`, `road`, `city`, `state`, `postcode`, `country`, `countryCode` | Broken-out address components |
| `extraTags` | Raw OSM tags: opening hours, website, phone, wheelchair access, brand, cuisine |
| `placeId` | Nominatim's internal ID. `null` for Overpass rows, which have no equivalent |
| `scrapedAt` | Run timestamp |

`RUN_SUMMARY` in the key-value store holds per-run counts, the filters you used, and any query that failed.

### Use cases

- **Bulk address geocoding.** Put a customer address list in, get coordinates and standardised components out.
- **Address validation.** Compare what you hold against `displayName` and the broken-out components.
- **Store locators and competitor mapping.** Sweep a city bounding box for `shop=supermarket` or `amenity=fuel`.
- **Reverse geocoding telemetry.** Turn GPS traces into street, city and postcode.
- **Enriching a places database.** Pull opening hours, phone and website from `extraTags`.

### Quick start

Geocode a few addresses:

```json
{
 "queries": ["Brandenburg Gate, Berlin", "10 Downing Street, London"],
 "contactEmail": "you@example.com"
}
```

Reverse-geocode coordinates and break out the components:

```json
{
 "coordinates": ["48.8584,2.2945", "52.5163,13.3777"],
 "includeAddressDetails": true,
 "contactEmail": "you@example.com"
}
```

Every cafe and bakery in central Berlin, with opening hours and phone numbers:

```json
{
 "poiTags": ["amenity=cafe", "shop=bakery"],
 "areaBoundingBox": { "south": 52.50, "west": 13.36, "north": 52.53, "east": 13.42 },
 "includeExtraTags": true,
 "maxResults": 2000,
 "contactEmail": "you@example.com"
}
```

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `queries` | array | `[]` | Free-text addresses or place names. Sent to Nominatim `/search`. |
| `coordinates` | array | `[]` | `"lat,lon"` strings. Sent to Nominatim `/reverse`. An unparseable pair fails the run immediately rather than silently dropping a row. |
| `poiTags` | array | `[]` | Tag filters like `amenity=cafe`, or a bare key like `amenity` to match any value. Sent to Overpass. Requires `areaBoundingBox`. |
| `areaBoundingBox` | object | none | `{ south, west, north, east }` in degrees. Only read when `poiTags` is set. |
| `countryCodes` | array | `[]` | Two-letter ISO codes restricting forward geocoding. Ignored for reverse and POI lookups. |
| `includeAddressDetails` | boolean | `true` | Break out house number, road, city, state, postcode and country. |
| `includeExtraTags` | boolean | `false` | Attach raw OSM tags. Always present on POI rows regardless. |
| `resultsPerQuery` | integer | `1` | Candidate matches kept per forward-geocoding query. Nominatim's own ceiling is 40. |
| `maxResults` | integer | `1000` | Cap on total places saved across the whole run. |
| `contactEmail` | string | none | Goes into the User-Agent. Strongly recommended, for the reasons below. |

You can combine all three modes in one run. Results are de-duplicated on `osmType` and `osmId`, so a place found by both a geocode and a POI sweep is saved once.

### Output example

```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"
}
```

Country and city names come back in the local language: `Deutschland`, not `Germany`. `countryCode` is the stable field to key on.

### Nominatim versus Overpass

The two endpoints answer different questions, and using the wrong one is how people get blocked:

| You want | Endpoint | Input to use |
|---|---|---|
| An address turned into coordinates | Nominatim `/search` | `queries` |
| Coordinates turned into an address | Nominatim `/reverse` | `coordinates` |
| Every object of a given kind in an area | Overpass | `poiTags` plus `areaBoundingBox` |

Nominatim's usage policy allows one request per second and explicitly asks that systematic and bulk queries go to Overpass instead. This Actor enforces the one-second gap in code, sends an identifying User-Agent on every request, and routes area sweeps to Overpass automatically. You do not have to remember any of that.

Overpass has no fixed per-second rate. It allocates query slots and will answer `429` or `504` when it is busy. Those are retried three times with linear backoff, and a `504` on the first attempt is normal for a large box and usually succeeds on the second.

### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/arman-bd~openstreetmap-places-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "queries": ["Brandenburg Gate, Berlin"],
 "includeAddressDetails": true,
 "contactEmail": "you@example.com"
 }'
```

### JavaScript example

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/openstreetmap-places-scraper').call({
 poiTags: ['amenity=cafe'],
 areaBoundingBox: { south: 52.5, west: 13.36, north: 52.53, east: 13.42 },
 includeExtraTags: true,
 maxResults: 500,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const place of items) {
 console.log(`${place.name ?? '(unnamed)'} at ${place.latitude}, ${place.longitude} (${place.extraTags.opening_hours ?? 'hours unknown'})`);
}
```

### Notes

- Nominatim gets one request per second, always. The gap is applied after every call including failed ones, so a run cannot burst past the policy even when queries are erroring.
- A `contactEmail` is not decoration. OpenStreetMap uses the User-Agent to reach heavy users before blocking them, and without one you are anonymous traffic, which gets throttled first. The Actor logs a warning when it is missing.
- Ways and relations get a centroid. Overpass returns no coordinates for non-node objects, so the query asks for `out center` and the centroid lands in `latitude` and `longitude`. `boundingBox` is `null` for POI rows because centroids carry no extent.
- Overpass errors arrive as HTML. A malformed tag or a timeout produces an HTML page rather than JSON, so the Actor digs the real message out of the markup and reports it instead of throwing a parse error.
- One bad query won't kill the run. Failures land in `RUN_SUMMARY.failures`, and the Actor only errors out if every query fails.
- Public data only. OpenStreetMap data is published under the ODbL, and the attribution "© OpenStreetMap contributors" is required if you publish anything derived from it.

### FAQ

**Do I need an API key?** No. You supply no credentials.

**How fast can I geocode a list?** One address per second, by policy. Ten thousand addresses is a little under three hours. If you need it faster, run your own Nominatim instance, because the public one is a volunteer service.

**Why did my POI sweep return nothing?** Either the box is empty, or the tag is wrong. Check the tag on the OpenStreetMap wiki: it is `shop=supermarket`, not `amenity=supermarket`.

**Why did Overpass time out?** The box was too large for the tag. Split it into quadrants and run them as separate queries, because a city-wide `amenity=restaurant` sweep is a lot of geometry.

**Can I get opening hours and phone numbers?** Yes, in `extraTags`, when mappers have recorded them. Coverage varies enormously by country and by category.

**Can I use this commercially?** Yes, under the ODbL: attribute OpenStreetMap and share alike if you redistribute a derived database. Read the licence before you build a product on it.

# Actor input Schema

## `queries` (type: `array`):

Free-text addresses or place names to geocode. Anything the OpenStreetMap search box accepts works here. Leave empty to skip forward geocoding.

## `coordinates` (type: `array`):

"lat,lon" pairs turned back into addresses, e.g. "52.5163,13.3777". Leave empty to skip reverse geocoding.

## `poiTags` (type: `array`):

OpenStreetMap tag filters such as amenity=cafe or shop=supermarket. Requires an area bounding box. Every node, way and relation carrying any of these tags inside the box is returned.

## `areaBoundingBox` (type: `object`):

The area POI tags are searched in: { south, west, north, east } in WGS84 degrees. Only read when poiTags is set. Keep it tight, because Overpass times out on city-scale boxes with common tags.

## `countryCodes` (type: `array`):

Two-letter ISO country codes that forward geocoding is confined to, e.g. de, gb. Leave empty to search worldwide. Ignored for reverse geocoding and POI searches.

## `includeAddressDetails` (type: `boolean`):

Split the result into house number, road, city, state, postcode and country instead of only the single display name. Costs nothing extra.

## `includeExtraTags` (type: `boolean`):

Attach the object's raw OpenStreetMap tags: opening hours, website, phone, wheelchair access, brand and whatever else the mappers recorded. Always present for POI searches.

## `resultsPerQuery` (type: `integer`):

How many candidate matches to keep for each forward-geocoding query. Nominatim never returns more than 40. Use 1 when you are standardising a known address list.

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

Cap on total places saved across every query in the run.

## `contactEmail` (type: `string`):

Added to the User-Agent sent to OpenStreetMap. Their usage policy asks for a way to reach whoever is running the queries, and giving one means they warn you before blocking rather than after.

## Actor input object example

```json
{
  "queries": [
    "10 Downing Street, London",
    "Eiffel Tower"
  ],
  "coordinates": [
    "52.5163,13.3777",
    "48.8584,2.2945"
  ],
  "poiTags": [
    "amenity=cafe",
    "shop=supermarket"
  ],
  "areaBoundingBox": {
    "south": 52.5,
    "west": 13.36,
    "north": 52.53,
    "east": 13.42
  },
  "countryCodes": [
    "de",
    "gb"
  ],
  "includeAddressDetails": true,
  "includeExtraTags": false,
  "resultsPerQuery": 1,
  "maxResults": 1000,
  "contactEmail": "you@example.com"
}
```

# Actor output Schema

## `items` (type: `string`):

Every record the run produced.

## `runsummary` (type: `string`):

The RUN_SUMMARY record from the run's key-value store.

# 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 = {
    "queries": [
        "Brandenburg Gate, Berlin"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arman-bd/openstreetmap-places-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 = { "queries": ["Brandenburg Gate, Berlin"] }

# Run the Actor and wait for it to finish
run = client.actor("arman-bd/openstreetmap-places-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 '{
  "queries": [
    "Brandenburg Gate, Berlin"
  ]
}' |
apify call arman-bd/openstreetmap-places-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arman-bd/openstreetmap-places-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/1BH5gu3r2HlMsFV6J/builds/zfTTD1RUgKmeFRfrq/openapi.json
