# Geocoding Scraper — Address ↔ Coordinates (OpenStreetMap) (`yadroo/osm-geocode`) Actor

Free geocoding for agents and CRMs via OpenStreetMap: address or place → lat/lon with full address breakdown, structured queries (street/city/postcode), reverse geocoding with zoom levels, OSM id lookup, bounded POI search, boundary polygons, multilingual names. Nominatim or Photon (fuzzy).

- **URL**: https://apify.com/yadroo/osm-geocode.md
- **Developed by:** [Samat Makatov](https://apify.com/yadroo) (community)
- **Categories:** Developer tools, Lead generation, Travel
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 result items

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

## Geocoding Scraper — address ↔ coordinates, POI search, boundaries (OpenStreetMap)

Free, key-less geocoding on OpenStreetMap data for agents, CRMs and data pipelines. Forward (address or place → lat/lon + full address breakdown), **structured** queries (street/city/postcode fields for messy CRM data), reverse (`lat,lon` → address at the zoom level you need), OSM id lookup, bounded POI search inside a box, boundary polygons as GeoJSON and names in any language. Two engines: **Nominatim** (exact, rich) and **Photon** (typo-tolerant). Worldwide, no API key, no proxy.

### Use cases

- **CRM address cleaning** — geocode 500 customer addresses with `structuredQueries`, keep `found`, `lat/lon`, normalized `postcode`/`city`; with `emitNotFound: true` rows stay 1:1 with input.
- **Store / branch enrichment** — `reverseZoom: 10` turns delivery GPS points into city names for territory reports.
- **Local lead lists** — `queries: ["pharmacy"]` + `viewbox` + `bounded: true` lists every mapped pharmacy in a district with street and house number; `extraTags` adds opening hours and websites where mapped.
- **Service-area maps** — `polygon: true` gives district/city boundaries as GeoJSON for coverage checks.
- **Multilingual labels** — `lang: "ru"` or `nameDetails: true` for Cyrillic / Kazakh / local names on the same object.
- **Dedupe & entity resolution** — stable `osmType`+`osmId`, `placeRank`, `importance` and `bestMatch` to pick the right candidate among ambiguous names.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `queries` | string\[] | — | Text address/place → forward; `lat,lon` → reverse; `R62422` / `N1,W2` → lookup. |
| `structuredQueries` | object\[] | — | `{ amenity, street, city, county, state, country, postalcode, label }` (Nominatim). |
| `mode` | select | `auto` | `auto`, `forward`, `reverse`, `lookup`. |
| `engine` | select | `nominatim` | `nominatim` or `photon` (structured & lookup always use Nominatim). |
| `lang` | string | `en` | Accept-Language, e.g. `ru`, `kk`, `de`, `ru,en`. Photon: `en`/`de`/`fr` only. |
| `countryCodes` | string | — | `kz,ru` — ISO-3166 alpha-2, comma-separated. |
| `viewbox` | string | — | `minLon,minLat,maxLon,maxLat`, e.g. Almaty `76.75,43.15,77.10,43.40`. |
| `bounded` | bool | `false` | Only results inside `viewbox` (enables area POI search). |
| `featureType` | select | — | `country`, `state`, `city`, `settlement`. |
| `layers` | multi | — | `address`, `poi`, `railway`, `natural`, `manmade`. |
| `limit` | int | `1` | Results per query; Nominatim max 40, Photon max 50. |
| `reverseZoom` | int | `18` | 18 building · 17 street · 16 major street · 14 suburb · 12 town · 10 city · 8 county · 5 state · 3 country. |
| `addressDetails` | bool | `true` | `address` object + derived columns. |
| `extraTags` | bool | `false` | `extraTags`: website, opening\_hours, wikidata, population… |
| `nameDetails` | bool | `false` | `nameDetails`: `name:ru`, `name:kk`, `official_name`… |
| `polygon` | bool | `false` | `geojson` outline. |
| `polygonThreshold` | number | `0` | Simplification tolerance in degrees (0.005 ≈ city level). |
| `dedupe` | bool | `true` | Nominatim result dedupe. |
| `emitNotFound` | bool | `false` | Emit `found: false` rows to keep 1:1 alignment. Such rows are charged like hits, so this is opt-in; not-found queries are always listed free in `SUMMARY.notFoundInputs`. |
| `outputFields` | string\[] | — | Projection, e.g. `["query","lat","lon","countryCode"]`. |
| `maxItems` | int | `10000` | Hard cap. |
| `osmTag` | string | — | Photon: `amenity:cafe`, `shop`, `!amenity:parking`. |
| `photonLayers` | multi | — | Photon: `house`, `street`, `locality`, `district`, `city`, `county`, `state`, `country`, `other`. |
| `biasLat`, `biasLon` | number | — | Photon: prefer results near this point. |

### Reference

#### Nominatim vs Photon

| | Nominatim | Photon |
|---|---|---|
| Matching | exact tokens, structured fields | fuzzy / prefix (typos OK) |
| Address breakdown | full (`address` object, 20+ keys) | street, housenumber, district, city, state, postcode, country |
| Reverse | yes, zoom-controlled | yes (nearest object) |
| OSM id lookup, polygons, extra tags, name details | yes | no |
| Language | any (`accept-language`) | en / de / fr |
| POI filter | `layers`, `featureType`, `bounded` box | `osm_tag`, `layer`, `bbox`, location bias |
| Policy | 1 request/s, identify app (the actor does both) | fair use |

#### `reverseZoom` levels

| zoom | returns |
|---|---|
| 3 | country |
| 5 | state |
| 8 | county |
| 10 | city |
| 12 | town / borough |
| 13 | village / suburb |
| 14 | neighbourhood |
| 16 | major streets |
| 17 | major and minor streets |
| 18 | building |

#### Common `type` / `category` values

`boundary/administrative` (countries, regions, cities, districts) · `place/city|town|village|suburb|neighbourhood` · `highway/residential|primary|…` (streets) · `building/yes|office|apartments|…` · `amenity/pharmacy|cafe|bank|school|…` · `shop/*` · `tourism/*` · `office/*`. Full tag catalogue: https://wiki.openstreetmap.org/wiki/Map\_features

### Examples

**Batch-geocode CRM addresses (1:1 rows)**

```json
{
  "structuredQueries": [
    { "street": "Nauryzbai Batyr 154A", "city": "Almaty", "country": "Kazakhstan", "label": "Kaspi HQ" },
    { "street": "Mangilik El 55", "city": "Astana", "country": "Kazakhstan", "label": "Client 2" }
  ],
  "lang": "ru",
  "emitNotFound": true,
  "outputFields": ["query", "found", "lat", "lon", "displayName", "postcode", "city", "osmUrl"]
}
```

**All pharmacies in a district (POI lead list)**

```json
{ "queries": ["pharmacy"], "viewbox": "76.85,43.20,77.00,43.30", "bounded": true, "limit": 40, "extraTags": true, "lang": "ru" }
```

**GPS points → city names for reporting**

```json
{ "queries": ["43.2363,76.9457", "51.1282,71.4307", "42.3417,69.5901"], "reverseZoom": 10, "outputFields": ["query", "city", "state", "countryCode"] }
```

**City boundaries as GeoJSON**

```json
{ "queries": ["Almaty", "Astana"], "featureType": "city", "countryCodes": "kz", "polygon": true, "polygonThreshold": 0.005 }
```

**Fuzzy search of user-typed input (Photon)**

```json
{ "queries": ["Almty Nauryzbai Batyr 154"], "engine": "photon", "limit": 3, "biasLat": 43.24, "biasLon": 76.95 }
```

### Output

```json
{
  "query": "Nauryzbai Batyr 154A, Almaty",
  "mode": "forward",
  "engine": "nominatim",
  "rank": 1,
  "bestMatch": true,
  "found": true,
  "placeId": 213728564,
  "osmType": "way",
  "osmId": 51267846,
  "osmUrl": "https://www.openstreetmap.org/way/51267846",
  "displayName": "Kaspi.kz, 154А, Nauryzbai Batyr Street, Керемет, Көктем, Bostandıq District, Almaty, 050013, Kazakhstan",
  "name": "Kaspi.kz",
  "lat": 43.2399856,
  "lon": 76.9388247,
  "category": "building",
  "type": "office",
  "addressType": "building",
  "placeRank": 30,
  "importance": 0.0000743,
  "boundingBox": [43.2398836, 43.2400877, 76.9383802, 76.9392699],
  "country": "Kazakhstan",
  "countryCode": "KZ",
  "state": null,
  "county": null,
  "city": "Almaty",
  "district": "Bostandıq District",
  "suburb": "Көктем",
  "postcode": "050013",
  "road": "Nauryzbai Batyr Street",
  "houseNumber": "154А",
  "address": { "building": "Kaspi.kz", "house_number": "154А", "road": "Nauryzbai Batyr Street", "city": "Almaty", "ISO3166-2-lvl4": "KZ-75", "postcode": "050013", "country": "Kazakhstan", "country_code": "kz" },
  "extraTags": { "building:levels": "7" },
  "nameDetails": null,
  "geojson": null,
  "licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
  "sourceUrl": "https://nominatim.openstreetmap.org/search?q=…",
  "fetchedAt": "2026-09-12T23:50:01.000Z"
}
```

| Field | Description |
|---|---|
| `query`, `mode`, `engine` | Input label (or structured `label`), detected mode, engine used. |
| `rank`, `bestMatch`, `found` | 1-based rank; rank 1 is `bestMatch`; `found: false` rows carry nulls. |
| `placeId`, `osmType`, `osmId`, `osmUrl` | Stable OSM identity (use `osmType+osmId` as key; `placeId` changes between Nominatim imports). |
| `displayName`, `name` | Full label and object name. |
| `lat`, `lon`, `boundingBox` | WGS84; bbox as `[minLat, maxLat, minLon, maxLon]` (Nominatim order). |
| `category`, `type`, `addressType`, `placeRank`, `importance` | OSM class/type, address level, 0–1 relevance. |
| `country`…`houseNumber` | Normalized address columns; raw parts in `address`. |
| `extraTags`, `nameDetails`, `geojson` | Only when requested. |
| `structured` | Echo of the structured query object (structured rows only). |

`SUMMARY` key-value record: `{ queries, found, notFound, rows, errors[{query, error}], engine, errorCount, notFoundCount, notFoundInputs[], notFoundRowsEmitted }`.

### Use it from code / agents

```bash
curl -X POST "https://api.apify.com/v2/acts/yadroo~osm-geocode/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' -d '{"queries":["Almaty, Kazakhstan","43.2363,76.9457"],"lang":"ru"}'
```

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/osm-geocode').call({ structuredQueries: [{ street: 'Nauryzbai Batyr 154A', city: 'Almaty', country: 'Kazakhstan' }] });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

```python
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/osm-geocode").call(run_input={"queries": ["pharmacy"], "viewbox": "76.85,43.20,77.00,43.30", "bounded": True, "limit": 40})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

MCP: connect `https://mcp.apify.com` and call the `yadroo/osm-geocode` tool with the same JSON.

### Pricing

Pay per event: **$0.001 per run start + $0.001 per result row** (not-found rows are included only when you turn `emitNotFound` on; failed queries are never charged). 100 addresses at `limit: 1` ≈ $0.10; a bounded POI search returning 40 rows ≈ $0.041.

### Limits & FAQ

- **Speed.** Nominatim's usage policy allows 1 request per second; the actor enforces a 1.1 s gap, so 300 queries ≈ 5.5 minutes. Split big batches into parallel runs of ≤ 500 queries.
- **Accuracy.** OSM coverage varies: exact house numbers exist for most cities in Europe/CIS/US; rural addresses may resolve to the street or village (check `addressType`/`placeRank`).
- **Ambiguity.** Use `countryCodes`, `viewbox`, `featureType` or `structuredQueries`; inspect `importance` and `rank` when `limit > 1`.
- **Errors.** HTTP 429/5xx → 3 retries with backoff; a failing query is listed in `SUMMARY.errors` (no row, not charged) and the run continues. Nothing fetched at all → run fails with the reason.
- **Licence.** Data © OpenStreetMap contributors, ODbL — attribution required when you display it; Nominatim results may not be used for autocomplete-as-you-type (use Photon for that).
- **Roadmap.** Batch mode with your own Nominatim/Photon endpoint, distance/nearest computation between rows.

***

Made by **Yadroo** · Sibling actors: [open-meteo-weather](https://apify.com/yadroo/open-meteo-weather) · [ip-intel](https://apify.com/yadroo/ip-intel) · [public-holidays](https://apify.com/yadroo/public-holidays) · [krisha-kz](https://apify.com/yadroo/krisha-kz)

# Actor input Schema

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

Free-text address or place name → forward geocoding; `lat,lon` → reverse geocoding; OSM ids like `R62422` / `N123,W456` → lookup (Nominatim). One request per entry, 1 request/second (Nominatim policy).

## `structuredQueries` (type: `array`):

Precise address parts instead of one string: objects with any of `amenity`, `street` (house number + street), `city`, `county`, `state`, `country`, `postalcode`, optional `label`. Better hit-rate for CRM addresses than free text.

## `mode` (type: `string`):

Force one mode for all queries or let the actor detect it per entry.

## `engine` (type: `string`):

Both are free OSM-based geocoders. Nominatim: exact matching, rich address breakdown, 1 req/s. Photon: fuzzy matching (good for messy user input), fewer address fields, languages en/de/fr only. Structured queries and OSM lookups always use Nominatim.

## `lang` (type: `string`):

Accept-Language for names, e.g. `en`, `ru`, `kk`, `de`, or a list `ru,en`. Photon supports only en/de/fr (otherwise local names).

## `countryCodes` (type: `string`):

Comma-separated ISO-3166 alpha-2 codes, e.g. `kz,ru`. Empty = worldwide.

## `viewbox` (type: `string`):

Prefer results inside this box, e.g. Almaty: `76.75,43.15,77.10,43.40`. With `bounded` = true results outside are excluded (turns a search like `pharmacy` into an in-area POI search).

## `bounded` (type: `boolean`):

Nominatim: exclude results outside `viewbox`.

## `featureType` (type: `string`):

Nominatim: return only places of this administrative type.

## `layers` (type: `array`):

Nominatim: restrict to `address`, `poi`, `railway`, `natural`, `manmade`. Empty = all.

## `limit` (type: `integer`):

Nominatim max 40, Photon max 50. `bestMatch` marks rank 1.

## `reverseZoom` (type: `integer`):

Nominatim reverse: 18 = building, 17 = street, 16 = major street, 14 = suburb, 12 = town/borough, 10 = city, 8 = county, 5 = state, 3 = country.

## `addressDetails` (type: `boolean`):

Include the `address` object and derived `country`, `city`, `road`… columns.

## `extraTags` (type: `boolean`):

Nominatim: `extraTags` with wikidata, website, opening\_hours, building:levels, population… when mapped.

## `nameDetails` (type: `boolean`):

Nominatim: `nameDetails` with `name:ru`, `name:kk`, `official_name`, `alt_name`…

## `polygon` (type: `boolean`):

Nominatim: add the outline of the place as `geojson` (city/district boundaries, building footprints). Can be large — combine with `polygonThreshold`.

## `polygonThreshold` (type: `number`):

Degrees; 0.005 ≈ city-level simplification. 0 = full detail.

## `dedupe` (type: `boolean`):

Nominatim: merge the same street/place appearing as several OSM objects.

## `emitNotFound` (type: `boolean`):

Writes a `found: false` row for each query with no match, keeping input/output aligned 1:1. Each such row is a dataset item and is charged like a hit, so it is off by default; not-found queries are always listed free in the SUMMARY record. Failed queries (errors) never produce rows.

## `outputFields` (type: `array`):

Optional projection, e.g. `["query","lat","lon","displayName","countryCode"]`.

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

Hard cap on dataset rows.

## `osmTag` (type: `string`):

Photon only. Filter/boost by tag: `amenity:cafe`, `tourism`, `!amenity:parking` (exclude), `shop:supermarket`.

## `photonLayers` (type: `array`):

Photon only: restrict result granularity.

## `biasLat` (type: `number`):

Photon only: prefer results near this point (with biasLon).

## `biasLon` (type: `number`):

Photon only.

## Actor input object example

```json
{
  "queries": [
    "Almaty, Kazakhstan",
    "Nauryzbai Batyr 154A, Almaty",
    "43.2363,76.9457",
    "R62422"
  ],
  "structuredQueries": [
    {
      "street": "Nauryzbai Batyr 154A",
      "city": "Almaty",
      "country": "Kazakhstan",
      "label": "Kaspi HQ"
    }
  ],
  "mode": "auto",
  "engine": "nominatim",
  "lang": "en",
  "bounded": false,
  "featureType": "",
  "limit": 1,
  "reverseZoom": 18,
  "addressDetails": true,
  "extraTags": false,
  "nameDetails": false,
  "polygon": false,
  "polygonThreshold": 0,
  "dedupe": true,
  "emitNotFound": false,
  "maxItems": 10000
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

# 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": [
        "Almaty, Kazakhstan",
        "Nauryzbai Batyr 154A, Almaty",
        "43.2363,76.9457",
        "R62422"
    ],
    "structuredQueries": [
        {
            "street": "Nauryzbai Batyr 154A",
            "city": "Almaty",
            "country": "Kazakhstan",
            "label": "Kaspi HQ"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("yadroo/osm-geocode").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": [
        "Almaty, Kazakhstan",
        "Nauryzbai Batyr 154A, Almaty",
        "43.2363,76.9457",
        "R62422",
    ],
    "structuredQueries": [{
            "street": "Nauryzbai Batyr 154A",
            "city": "Almaty",
            "country": "Kazakhstan",
            "label": "Kaspi HQ",
        }],
}

# Run the Actor and wait for it to finish
run = client.actor("yadroo/osm-geocode").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": [
    "Almaty, Kazakhstan",
    "Nauryzbai Batyr 154A, Almaty",
    "43.2363,76.9457",
    "R62422"
  ],
  "structuredQueries": [
    {
      "street": "Nauryzbai Batyr 154A",
      "city": "Almaty",
      "country": "Kazakhstan",
      "label": "Kaspi HQ"
    }
  ]
}' |
apify call yadroo/osm-geocode --silent --output-dataset

```

## MCP server setup

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

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/S2JxnDI1OgwXYCtJY/builds/KRlE22a45t3zLMo1H/openapi.json
