---
title: "Location Blocks · RUAL Documentation"
description: "Offline geocoding, coordinate maths and points of interest: every geo point, places and map block in one place."
canonical: https://docs.rual.nl/reference/location-blocks
language: en
---

# Location Blocks

Everything RUAL knows about *where* something is, and *what is near it*. Coordinate maths, address lookup and points of interest, all answered from datasets on the node.

These blocks run **offline**, against datasets shipped with the node. No API keys, no rate limits, no per-call cost, and no request leaving the building. Lookups take microseconds, so you can run them inside loops and over whole inventories without thinking about it.

| Operation | Time |
| --- | --- |
| Nearest lookup (kNN), for example `places_nearest` | 6 to 8 µs. A rare category is as fast as a common one. |
| Text search, `places_search` | 11 ms |

| Family | What it does |
| --- | --- |
| [`geo_point_*`](#coordinates) | Create, convert and do arithmetic on coordinates. |
| [`geo_point_reverse_geocode`, `array_reverse_geocode`, `geo_point_from_city`](#geocoding) | Turn a coordinate into an address, or a place name into a coordinate. |
| [`places_*`](#places) | Find points of interest near a coordinate, search them, filter by them. |
| [`osmworld_*`](#map-geometry) | Fetch map geometry for rendering. |

### Working With Coordinates

A **geo point** is an object with `lat` and `lon`. These blocks make and manipulate one. None of them touch a dataset: they are pure arithmetic.

| Block | Does what | Pins |
| --- | --- | --- |
| [`geo_point_lat_lng_to_point`](https://docs.rual.nl/block-types/geopoint/geo_point_lat_lng_to_point) | Builds a point from two numbers. | in `lat`, `lng` out `geo_point` |
| [`geo_point_to_lat_lng`](https://docs.rual.nl/block-types/geopoint/geo_point_to_lat_lng) | Splits a point back into numbers. | in `geo_point` out `latitude`, `longitude` |
| [`geo_point_convert_to_object`](https://docs.rual.nl/block-types/geopoint/geo_point_convert_to_object) | Casts a point to a plain object. | in `geo_point` out `object` |
| [`geo_point_validate`](https://docs.rual.nl/block-types/geopoint/geo_point_validate) | Answers whether this is actually a usable coordinate. | in `geo_point` out `valid` |
| [`geo_point_branch`](https://docs.rual.nl/block-types/geopoint/geo_point_branch) | Picks one of two points on a condition. | in `then`, `else`, `condition` out `geo_point` |
| [`geo_point_destination_point`](https://docs.rual.nl/block-types/geopoint/geo_point_destination_point) | Moves a point a given distance along a bearing. | in `geo_point`, `distance` (metres), `current_bearing` (degrees) out `geo_point` |
| [`geo_point_random_in_radius`](https://docs.rual.nl/block-types/geopoint/geo_point_random_in_radius) | Returns a random point within a radius. | in `geo_point`, `radius` out `geo_point`, `success`, `error` |
| [`geo_point_define_reference`](https://docs.rual.nl/block-types/references/geo_point_define_reference) / [`geo_point_use_reference`](https://docs.rual.nl/block-types/references/geo_point_use_reference) | Names a point once and reuses it elsewhere in the blueprint without rewiring. | see the block pages |

Use [`geo_point_validate`](https://docs.rual.nl/block-types/geopoint/geo_point_validate) as a guard before anything that costs work: a missing or malformed point is the most common reason a location flow silently does nothing. Use [`geo_point_random_in_radius`](https://docs.rual.nl/block-types/geopoint/geo_point_random_in_radius) to anonymise a location before showing it publicly, or to generate plausible test data.

### Geocoding: Coordinates To Places, And Back

#### A coordinate becomes an address

[`geo_point_reverse_geocode`](https://docs.rual.nl/block-types/geopoint/geo_point_reverse_geocode) takes a `geo_point` and returns `street`, `city`, `neighborhood`, `province`, `country`, `country_code`, `success` and `error`.

- `neighborhood` is filled only when the point sits inside a larger city. A point in Ittersum returns city *Zwolle* and neighbourhood *Ittersum*, while a point in Siebengewald returns that as the city, because it is a village in its own right rather than part of one.

- `street` is the nearest named road and needs the road network dataset loaded.

#### Many coordinates at once

[`array_reverse_geocode`](https://docs.rual.nl/block-types/array/array_reverse_geocode) enriches a whole array in one call, with no loop. You give it the path to the geo point inside each item (for example `location.location`) and the paths to write each result to. Leave an output key empty to skip that field.

| Pin | Purpose |
| --- | --- |
| `array` | The items to enrich. |
| `location_key` | Path to the geo point inside each item. |
| `location_fallback_key` | Second path, for the common case where some records carry a verified coordinate and others only a rough one. |
| `street_key`, `city_key`, `neighborhood_key`, `province_key`, `country_key`, `country_code_key` | Where to write each resolved field. |
| `array` (out) | The enriched items. |
| `unresolved` (out) | Items whose point was missing or outside coverage. |

### Always check unresolved

Items whose point was missing or outside coverage come back **unchanged**. If you write the array to a store without looking at `unresolved`, you will not notice.

#### A place name becomes a coordinate

[`geo_point_from_city`](https://docs.rual.nl/block-types/geopoint/geo_point_from_city) takes `city` and `country_code`, and returns `geo_point`, `radius`, `province`, `country`, `country_code`, `success` and `error`.

- `radius` is the city's approximate extent in metres, derived from population. Handy for "is this roughly in Breda" without needing a boundary.

- Pass `country_code` to disambiguate: there is a Bergen in NL, NO and DE.

#### Terrain and infrastructure

| Block | Answers | Pins |
| --- | --- | --- |
| [`geo_point_land_cover`](https://docs.rual.nl/block-types/geopoint/geo_point_land_cover) | What kind of ground is this? | in `geo_point` out `type` (urban, agricultural, forest, grassland, wetland, water, bare, coastal), `code`, `success`, `error` |
| [`geo_point_nearby`](https://docs.rual.nl/block-types/geopoint/geo_point_nearby) | Is a road or railway within X metres? | in `geo_point`, `type` (road/rail), `max_distance` out `is_nearby`, `distance`, `nearest_point`, `success`, `error` |
| [`geo_point_snap`](https://docs.rual.nl/block-types/geopoint/geo_point_snap) | Pull a point onto the nearest road or railway. | in `geo_point`, `type` out `geo_point`, `distance`, `success`, `error` |

On [`geo_point_snap`](https://docs.rual.nl/block-types/geopoint/geo_point_snap), `distance` tells you how far the point moved. A large value means the original point was nowhere near a road, which usually means the input was wrong.

### Places: Points Of Interest

The `places_*` blocks are backed by **Overture Maps Places**, a source of roughly **60 million** points of interest in total. The Netherlands is what a node loads by default: roughly 737,000 Dutch shops, schools, stations, restaurants, hospitals and so on. Three ideas explain most of how these blocks behave, and they are worth reading before wiring anything.

### Need another country?

Only the Dutch extract is loaded by default, but the source covers the rest of the world. Other countries can be added to your node on request: ask through [support](https://docs.rual.nl/support) and we will build and ship that extract.

#### Distances, never yes or no

Every spatial block returns **the distance in metres**, not a boolean. That is deliberate: holding the distances, every question is arithmetic you can do yourself, with no second query.

```
within 200 m of a school?         nearest_distance_m <= 200
within 317 m?                     nearest_distance_m <= 317
at least 3 schools within 500 m?  results[2].distance_m <= 500
NOT within 500 m of a school?     nearest_distance_m > 500
```

So a radius slider in your UI never re-queries, and no radius is ever baked into stored data where changing it would mean reprocessing everything.

#### Categories accept plain Dutch

Anywhere a block has a `categories` pin, you can pass group ids or ordinary words. Regular plurals are handled.

| Group id | Also accepted |
| --- | --- |
| `primary_education` | `basisschool`, `basisscholen` |
| `bar` | `kroeg`, `kroegen` |
| `train_station` | `treinstation`, `station` |

`places_categories` lists everything available, and `places_resolve_category` turns one word into a group id.

#### Duplicates are collapsed, and the result says so

Overture is not deduplicated. Around Breda station the raw data holds three separate records for one station: `Station Breda Centraal`, `Gare de Bréda` and `NS Station Breda`. With `dedup` on (the default) those merge into one result, reported as `merged_count: 3` with `merged_names` listing all three. Meanwhile the bike shed, the car park, the park called *Happy Station*, the Playmobil museum and the neighbourhood council all stay separate: they are different things that happen to share a word.

`places_within` reports both `raw_count` and `count`. The gap between them tells you how much duplication the source data carries at that spot. Treat `count` as a tidy search result, not a census: nationally the data holds 1,432 `train_station` records against roughly 400 real stations.

#### Finding places

| Block | Does what | Pins |
| --- | --- | --- |
| `places_nearest` | The one you will use most: nearest matches with their distance. | in `geo_point` (required), `categories`, `max_results` (10), `max_distance` (0 = unbounded), `min_confidence`, `dedup` (true) out `results`, `nearest`, `nearest_distance_m`, `count`, `success`, `error` |
| `places_within` | Everything inside a radius. | in `geo_point` (required), `radius_m` (required), `categories`, `limit` (100), `sort` (distance/confidence/name), `min_confidence`, `dedup` out `results`, `count`, `raw_count`, `truncated`, `success`, `error` |
| `places_in_bbox` | Everything on screen. | in `north_east` (required), `south_west` (required), `categories`, `limit` (500), `min_confidence`, `dedup` out `results`, `count`, `raw_count`, `truncated`, `success`, `error` |
| `places_density` | How built up is this area? | in `geo_point` (required), `radius_m` (500), `categories` out `total`, `raw_total`, `per_km2`, `by_group`, `top_groups`, `classification`, `success`, `error` |

Results from `places_in_bbox` carry no distance: a box has no centre. Use `places_nearest` when you need distances. On `places_density`, `classification` is `dense_urban`, `urban`, `suburban` or `rural`, driven by the per square kilometre rate, so it does not change when you change the radius.

#### Working over a whole inventory

These blocks exist so you never write a loop.

| Block | Does what | Pins |
| --- | --- | --- |
| `places_enrich` | Adds nearest-place data to every item in a list, in one call. | in `items` (required), `point_path`, `categories` (required), `max_distance`, `count_radius`, `include`, `output_path` (`places`) out `items`, `count`, `unresolved`, `success`, `error` |
| `places_filter` | Keeps or drops items by a places rule, without touching a store. | in `items` (required), `point_path`, `filter` (required), `mode` (keep/remove), `annotate`, `output_path` out `items`, `count`, `removed_count`, `success`, `error` |
| `places_facet` | Answers "how many of my objects are near a hospital?" | in `items` (required), `point_path`, `categories`, `radius_m` (500), `filters` out `counts`, `total`, `success`, `error` |

Each item that `places_enrich` touches gets one entry per category:

```
{
  "places": {
    "hospital": {
      "count": 0,
      "distance_m": 1840,
      "id": "...",
      "name": "..."
    },
    "primary_education": {
      "count": 3,
      "distance_m": 212,
      "id": "...",
      "name": "..."
    }
  }
}
```

Items are copied, never modified in place, and every item comes back whether or not it had a usable coordinate. Check `unresolved` before writing back. `places_facet` counts **items, not places**: an object with three hospitals nearby counts once.

#### Search and typed questions

`places_search` is one search box over places *and* towns. It takes `query` (required), `near`, `bias_radius_m` (25000), `kinds` (poi/settlement), `categories`, `limit` (10), `typo_tolerance` (2), `min_confidence` and `dedup`, and returns `results`, `best`, `count`, `success` and `error`.

- Handles accents (`Bréda` finds `Breda`), typos (`amsterdm centraal`) and partial words as you type.

- Towns outrank businesses, so `Breda` gives you the municipality rather than one of the roughly 2,000 companies with Breda in the name.

- Pass `near` (your map centre) to prefer nearby results.

- Results carry `kind` (`poi` or `settlement`), `subtitle` (`Breda, Noord-Brabant`, or `treinstation · Amsterdam`) and `score`: enough to render a dropdown without a second lookup.

`places_parse_query` turns typed Dutch or English into a filter. It takes `query` (required), `language` (auto), `default_radius_m` (500) and `near`, and returns `kind`, `filter`, `negated`, `category_group`, `category_label`, `radius_m`, `min_count`, `geo_point`, `place_name`, `confidence`, `unparsed`, `success` and `error`.

| Typed | Parsed as |
| --- | --- |
| `binnen 300m van een basisschool` | proximity, `primary_education`, 300 m |
| `niet bij scholen` | proximity, `education`, negated |
| `minimaal 3 kroegen binnen 500m` | proximity, `bar`, 500 m, at least 3 |
| `wijk Jordaan` | an area, with coordinates |
| `Station Breda` | a place, with coordinates |

Parsing is rule-based and deterministic: no model call, no network, the same answer every time. It never guesses silently, so anything it could not read comes back in `unparsed` with a lower `confidence`, and you can fall back to plain search rather than acting on half a filter.

`places_match` tests one point against a filter. It takes `geo_point` (required), `filter` (required) and `max_distance`, and returns `matches`, `distance_m`, `count`, `nearest`, `reason`, `success` and `error`. Both `distance_m` and `count` come back **whether or not it matched**, so a radius slider can re-answer from the result without calling again. `reason` is a readable Dutch line you can show the user, for example `3× basisschool binnen 500m; dichtstbij 212m`.

#### Categories and dataset info

| Block | Does what | Pins |
| --- | --- | --- |
| `places_categories` | Builds your category dropdown. | in `search`, `group`, `language` (nl) out `groups`, `count`, `success`, `error` |
| `places_resolve_category` | Resolves one word to a group, and returns `alternatives` when a word is ambiguous (`school`) rather than picking one. | in `text` (required), `language` out `group`, `label`, `confidence`, `alternatives`, `success`, `error` |
| `places_get` | Re-resolves a stored id. | in `id` (required) out `place`, `found`, `success`, `error` |
| `places_dataset_info` | Reports what data this node is running. Worth putting on an admin screen. | no inputs out `loaded`, `release`, `vocabulary_version`, `record_count`, `group_count`, `category_count`, `bbox`, `built_at`, `success`, `error` |

### Two blocks want a different out-pin than success

Branch `places_get` on `found`, not `success`: an id can vanish between dataset releases, and that is a normal outcome rather than an error. Branch `places_dataset_info` on `loaded`, not `success`: it is the only places block that answers at all when no dataset is loaded.

### Map Geometry

[`osmworld_tile`](https://docs.rual.nl/block-types/openstreetmap/osmworld_tile) (in `z`, `x`, `y`, `kinds`) and [`osmworld_bbox`](https://docs.rual.nl/block-types/openstreetmap/osmworld_bbox) (in `bbox`, or `south`/`west`/`north`/`east`, plus `kinds` and `simplify`) both return a GeoJSON FeatureCollection of roads, buildings, water and greenery for drawing a map, on a `geojson` out-pin alongside `count`.

### These two are the exception

Unlike everything else on this page, the `osmworld_*` blocks call Overpass **over the network** and cache to disk. They have flow pins and they are not free. Budget for latency and failure the way you would for any outbound call.

### Recipes

#### Store how far every object is from a school

```
query objects
  -> places_enrich   items=results, point_path=general.location,
                     categories=basisschool, output_path=places
  -> mutations       write items back
```

Afterwards an ordinary [`query_bool_range_field`](https://docs.rual.nl/block-types/query/query_bool_range_field) on `places.primary_education.distance_m` filters your inventory. No further place lookups, and any radius works, because the distance is stored rather than a flag.

#### A search box that understands what was typed

```
places_parse_query  query = <what the user typed>
  |- kind = place / area  -> geo_point   -> move the map
  |- kind = proximity     -> filter      -> places_filter over your objects
```

Use `confidence` and `unparsed` to decide whether to trust the filter or fall back to `places_search`.

#### How many of my objects are near a hospital?

```
query objects -> places_facet  categories = ziekenhuis, apotheek
                               radius_m   = 500
```

`counts` gives you the number per category, and `total` the number of items evaluated.

#### Guarding a location flow

```
geo_point_validate -> if not valid, stop
  -> geo_point_reverse_geocode   (address)
  -> places_nearest              (what is around it)
```

### Reading The Results Honestly

The data is good, but it is not a register. These are properties of the source, so meet them here rather than in front of a client.

| What | Why it matters |
| --- | --- |
| Education overcounts by about 6% | 6,704 records against DUO's roughly 6,300 primary schools. That is exactly the filter with legal consequences, so treat it as indicative rather than compliance grade, and check the official register before anything binding. |
| Counts are indicative | Deduplication tidies search results, it does not resolve entities. `raw_count` sits next to `count` so the gap stays visible. |
| `confidence` is uncalibrated | Median 0.80. Filtering on it may hide coverage as easily as it removes noise, which is why it defaults to no filter. |
| `brand` is sparse | Match chains on `name`. Kruidvat reads 1,010 by name and 27 by brand. |
| Areas are not official | `wijk Jordaan` resolves against OSM neighbourhood data, not CBS boundaries, and carries no official code. |

Every block on this page reports failures on its `error` pin rather than stopping the blueprint, so branch on `success` and handle the code.

| Error | Means |
| --- | --- |
| `PLACES_NOT_AVAILABLE` | No places dataset on this node. |
| `GEO_POINT_MISSING_LAT_OR_LON` | The point had no usable coordinate. |
| `RADIUS_REQUIRED` | A radius pin was left empty on a block that needs one. |
| `CATEGORIES_REQUIRED` | A categories pin was left empty on a block that needs one. |
| `FILTER_REQUIRED` | `places_filter` or `places_match` ran without a filter. |
| `QUERY_REQUIRED` | `places_search` or `places_parse_query` ran without a query. |

### The Datasets Behind All This

The datasets are shared by every entity on the node and loaded from disk at startup. They are gated by `geo.require_data`: with that on (the default) a node **refuses to boot** rather than quietly serving blocks backed by missing data.

| File | Powers | Source |
| --- | --- | --- |
| *(embedded)* | `geo_point_reverse_geocode`, `geo_point_from_city`, `array_reverse_geocode` | GeoNames + OSM |
| `network.bin` | `geo_point_nearby`, `geo_point_snap`, the `street` output | OpenStreetMap |
| `landcover.bin` | `geo_point_land_cover` | ESA WorldCover |
| `places.bin` | every `places_*` block | Overture Maps Places |

Because the files are loaded rather than compiled in, refreshing the data needs no new binary: just the file and a restart. You do not have to remember to refresh the places data either. The node checks the published Overture releases at startup and once a day after, and warns when it has fallen three releases behind.

```
places dataset is STALE: running release 2026-01-01.0, latest is 2026-07-22.0
(6 releases behind). Refresh it.
```

### Next Steps

### Frequently asked

**Does RUAL geocoding need a Google Maps API key?**

No. Reverse geocoding, city lookup, land cover and the places search all run offline against datasets shipped with the node, so there is no API key, no rate limit and no per-call cost. Only the osmworld blocks, which fetch map geometry for rendering, call out over the network.

**How do I find out whether a location is near a school in RUAL?**

Use places_nearest with the category basisschool. It returns nearest_distance_m, the distance in metres, rather than a yes or no. Compare that number to whatever radius you need, so changing the radius never means running the query again.

**How do I add the distance to the nearest school to every document in a RUAL storage?**

Run places_enrich over the search result with the path to the geo point inside each item, then write the items back with mutations. The stored distance can then be filtered with an ordinary range query, and it works for any radius because the distance is stored rather than a flag.

**Does RUAL have places data for countries other than the Netherlands?**

A node loads the Dutch extract by default, roughly 737,000 points of interest, out of a source that holds around 60 million worldwide. Other countries can be added to your node on request: ask through support and the extract is built and shipped to you.
