Authoring Osmose Rule DSL Checks Jump to heading
Surface a data-quality issue on the Osmose QA map — for example every amenity=fuel node with no name — by writing a backend analyser that selects the offending features with SQL and emits a typed error class the frontend can render as map markers.
Prerequisites Jump to heading
Have each of these in place; an analyser that runs cleanly but produces zero markers is nearly always a schema-name or class-registration slip below.
Conceptual minimum Jump to heading
Osmose is a server-side quality-assurance system: it ingests an OSM extract into PostgreSQL, runs a battery of analysers over the database, and publishes each analyser’s findings as issue markers on a web map. An analyser is a Python class, and the two families that matter are the Analyser_Osmosis base, which runs raw SQL against the osmosis snapshot schema, and the Analyser_Merge family, which cross-references OSM against an external open dataset. This page uses the Analyser_Osmosis pattern because most house rules are expressible as a query: the offending features are exactly the rows a SELECT returns, and the analyser’s job is to turn each returned row into a typed error. Unlike the editor-time checks in a JOSM validation preset, an Osmose check runs over the whole database on a schedule, so it catches issues in data nobody is actively editing.
Every issue Osmose raises is identified by a small set of fields that the frontend keys on. class_id is a number unique within the analyser; it groups all instances of one problem so mappers can filter by it. item is a broad category number (for example the ranges used for tagging, geometry, or routing problems) that colours and groups issues across analysers. level is severity from 1 (most serious) to 3 (minor). The analyser declares each class once in its constructor with a translated title, then, for every offending row the SQL returns, calls self.error with that class_id and the feature’s type, id, and coordinates. The backend writes those errors to its issue tables; the frontend reads them and drops a marker at each feature’s location. The pipeline below shows that path end to end.
Runnable solution Jump to heading
Save this as analyser_merge_fuel_name.py under the Osmose backend analysers/ directory. It declares one issue class and selects every fuel node lacking a name, emitting an error per row.
import logging
from modules.OsmoseTranslation import T_
from Analyser_Osmosis import Analyser_Osmosis
logger = logging.getLogger(__name__)
# SQL that selects offending features from the osmosis snapshot schema.
# nodes.tags is an hstore column; the ? operator tests key presence.
SQL_FUEL_WITHOUT_NAME = """
SELECT
nodes.id,
ST_X(nodes.geom) AS lon,
ST_Y(nodes.geom) AS lat
FROM
nodes
WHERE
nodes.tags ? 'amenity'
AND nodes.tags -> 'amenity' = 'fuel'
AND NOT (nodes.tags ? 'name')
"""
class Analyser_Merge_Fuel_Name(Analyser_Osmosis):
"""Flag fuel stations (amenity=fuel) that are missing a name tag."""
def __init__(self, config, logger=None):
super().__init__(config, logger)
# class_id: unique within this analyser. item: broad tagging category.
self.classs[1] = self.def_class(
item=3220,
level=3,
tags=["fuel", "fix:survey"],
title=T_("Fuel station without a name"),
detail=T_("This amenity=fuel node carries no name tag."),
fix=T_("Survey the station and add its name."),
)
def analyser_osmosis_common(self):
self.run(SQL_FUEL_WITHOUT_NAME, self._emit)
def _emit(self, res):
node_id, lon, lat = res[0], res[1], res[2]
# self.error groups by class_id and positions the marker at (lon, lat).
self.error(
self.classs[1],
{"self": lambda r: {"en": T_("Fuel station without a name")}},
subclass=1,
osm_id={"N": node_id},
geom={"position": [{"lat": lat, "lon": lon}]},
)
Run it against the loaded database from the backend root:
python osmose_run.py --analyser=merge_fuel_name --country=my-extract
Step-by-step walkthrough Jump to heading
- Imports and base class.
Analyser_Osmosisprovidesself.run(execute SQL and iterate rows) andself.error(emit an issue).T_marks strings for translation, which the frontend uses to localise titles. - The SQL contract. The query must return the columns the emit callback expects — here
id,lon,lat. Selecting the geometry asST_X/ST_Ygives the marker coordinates directly; without them the frontend has nowhere to place the issue. - hstore presence tests. In the
osmosisschema, tags live in an hstore column:tags ? 'name'tests key presence, andtags -> 'amenity'reads a value.NOT (tags ? 'name')is the SQL equivalent of the editor rule’s[!name]absence test. - Declaring the class.
self.classs[1] = self.def_class(...)registers one issue class.item=3220places it in the tagging-issue band,level=3marks it minor, andtitleis the label mappers read on the map. Declaring the class in the constructor means it exists even when a run finds zero issues. analyser_osmosis_common. This is the entry point Osmose calls for a database analyser.self.run(sql, callback)streams each result row into the callback rather than materialising the whole result set, which keeps memory bounded on continental extracts.- Emitting an error.
self.errorwrites one issue:subclassdisambiguates variants within a class,osm_id={"N": node_id}links the marker to the node so JOSM can open it, andgeom={"position": [...]}fixes the marker’s map location. - The item numbering.
itemvalues are shared conventions across all Osmose analysers so the frontend can group and colour by category; reusing an established band keeps your check consistent with the rest of the catalogue rather than inventing an orphan category. - Running and re-running. The command loads no data itself; it assumes the
osmosisschema is already populated, so a re-run after fixing tags in the database re-evaluates the same SQL and clears resolved markers.
Verification Jump to heading
Confirm the check produces the markers you expect:
- Class exists. After a run, the analyser’s
classtable must contain a row forclass_id1 with your title, even on a clean extract — proof the constructor registered it. - Row count matches errors. Run
SQL_FUEL_WITHOUT_NAMEby hand inpsql; the number of returned rows must equal the number of issues Osmose emitted for that class. - Marker placement. Open the generated issue output and confirm each marker’s
lat/lonsits on the corresponding fuel node, not at(0, 0)— a(0, 0)marker means the geometry columns were missing from the SELECT. - Severity renders. The issue should appear at level 3 (minor) styling on the map; a wrong
levelargument shows up as the wrong colour band. - Idempotent re-run. Add
nameto one node in the database, re-run, and confirm that node’s marker disappears while the others remain.
Common errors and fixes Jump to heading
| Symptom | Root cause | One-line fix |
|---|---|---|
| Analyser runs, zero markers | Query returns nothing due to wrong schema/table | Confirm the osmosis schema is loaded and nodes is populated. |
KeyError on self.classs |
self.error used a class_id never declared |
Register every class in __init__ via def_class. |
All markers at (0, 0) |
Geometry not selected or geom position empty |
Select ST_X/ST_Y and pass them in geom["position"]. |
operator does not exist: hstore ? unknown |
hstore extension not enabled | CREATE EXTENSION hstore; in the target database. |
| Title shows as raw msgid | String not wrapped for translation | Wrap user-facing text in T_(...). |
| Duplicate markers per feature | Callback emits inside a loop over joined rows | Ensure the SQL returns one row per offending feature. |
| Wrong colour grouping on map | item category number outside its band |
Reuse an established item band for the issue type. |
For issues that also need catching while a mapper edits, pair this server-side class with the editor guardrail in Writing Custom JOSM Validation Presets so both the live and the batch path enforce the same rule.
Specification reference Jump to heading
An Osmose backend analyser subclasses
Analyser_Osmosisand runs SQL against theosmosissnapshot schema; each issue class is registered withdef_classcarrying anitemcategory, alevelfrom 1 to 3, and a translatedtitle, and each offending feature is reported withself.errornaming the class, theosm_id, and ageomposition. See the Osmose overview on the OSM Wiki and the Osmose backend source and analyser docs for the analyser API, theitemnumbering conventions, and the translation helpers.
Frequently Asked Questions Jump to heading
When should I use Analyser_Osmosis versus Analyser_Merge?
Use Analyser_Osmosis when the offending features are fully describable by a SQL query over the OSM snapshot alone — missing tags, bad geometry relationships, inconsistent attributes. Reach for the Analyser_Merge family when the check compares OSM against an external open dataset, such as matching official addresses or public facility registries, because that family handles fetching, conflating, and diffing the reference source for you.
What do class_id, item, and level actually control?
class_id is unique within one analyser and groups every instance of a single problem so mappers can filter by it. item is a broad category number shared across all analysers that the frontend uses to colour and group issue types. level is severity from 1, the most serious, to 3, minor. Together they decide how an issue is labelled, coloured, grouped, and prioritised on the QA map.
Why does my analyser produce no markers even though the SQL looks right?
The usual cause is that the database was never loaded into the osmosis schema the analyser queries, so the tables are empty and the query returns nothing. Run the SELECT directly in psql first; if it returns zero rows there, the problem is the data load or the schema name, not the analyser. If psql returns rows but Osmose shows none, check that the emit callback passes valid geometry positions.
How do markers get their location on the map?
Each self.error call includes a geom argument with a position list of latitude and longitude pairs. Those coordinates come straight from the SQL, typically as ST_X(geom) and ST_Y(geom) for a node. The frontend drops one marker per emitted error at that position. If you omit the geometry columns from the SELECT, the markers collapse onto null-island at zero-zero.
Related Jump to heading
- Authoring OSM Validation Rules — the section that frames server-side and editor-side checks together.
- Writing Custom JOSM Validation Presets — the editor-time counterpart to this batch analyser.
- Building Python-Based OSM Validation Rules — a lighter-weight streaming framework when a full Osmose backend is overkill.
- Tag Taxonomy & Key-Value Standards — the tag conventions the SQL conditions encode.
- Flagging Deprecated OSM Tags in a Pipeline — a related tag-consistency check expressed as a pipeline step.
- OSM Data Quality & Validation — the surrounding quality-assurance section.
Up one level: Authoring OSM Validation Rules.