MapRoulette Tag Fix challenges
Example task: examples/tag-fix-task.ndjson. Docs: Tag Fix, external IDs, rebuild, line-by-line GeoJSON.
1. Emit line-by-line GeoJSON (because Tag Fix needs it)
Do: one task = one line = one FeatureCollection, each line prefixed with RFC 7464 RS (\x1E).
Because: Tag Fix cooperativeWork is ignored on traditional multi-feature GeoJSON; you get normal tasks instead.
const rs = String.fromCharCode(0x1e)
stream.write(`${rs}${JSON.stringify(taskFeatureCollection)}\n`)
2. Use one stable OSM type/id everywhere (because rebuild + editors need it)
Do: build osmId = "${type}/${id}" as a string (way/123, node/456, relation/789). Put the same value in:
| Place | Why |
|---|---|
features[0].properties.id |
External task ID; visible in task props; Mustache {{id}}; API lookup …/challenge/{id}/task/{type%2Fid} |
features[0].id |
Same ID on the feature (rebuild matching) |
cooperativeWork.operations[].data.id |
Tag Fix target element |
Because: MapRoulette matches rebuilds and editor preselect on that external ID. Wrong or mismatched IDs → duplicates or broken Tag Fixes.
Do not: use a bare numeric OSM id, a different string in cooperativeWork than on the feature, or {{osmIdentifier}} in instructions — use properties.id and {{id}} only (osmIdentifier is a legacy name in some older feeds).
3. Build Tag Fix cooperativeWork (because that is the proposed change)
Do:
"cooperativeWork": {
"meta": { "version": 2, "type": 1 },
"operations": [{
"operationType": "modifyElement",
"data": {
"id": "way/123",
"operations": [
{ "operation": "setTags", "data": { "cycleway:both": "no", "source": "official data" } },
{ "operation": "unsetTags", "data": ["old_tag"] }
]
}
}]
}
meta.type: 1= Tag Fix (approve/reject in MR; MapRoulette writes to OSM).type: 2= OSC cooperative (JOSM).- Only encode pending tag changes — not tags already on OSM. The Tag Fix UI may list current tags;
cooperativeWorkmust still be delta-only. - One
setTagscan set several keys on one element. UseunsetTagswhen removing tags (with tag names in the array) — omit emptyunsetTags: []. - Geometry is for the map pin only (
PointorLineString); it is not sent as the edit — onlycooperativeWorkis. - When Tag Fix auto-writes to OSM from official or third-party data, filter tasks by licence/compatibility before export — do not propose tags the data licence does not allow.
4. Wire task_markdown + challenge instruction (because body text is Mustache-templated)
Do — per task properties:
properties: {
id: osmId, // also used as {{id}} in the challenge instruction
task_markdown: markdownBody.replaceAll('\n', ' \n'), // space before \\n keeps MR markdown line breaks
task_updated_at: new Date().toISOString(),
}
Do — challenge instruction template (set once on create/update):
## Kontext {{id}}
{{task_markdown}}
(Letzte Aktualisierung der Aufgabe: {{task_updated_at}})
Because: Mustache in the challenge instruction pulls feature properties. Putting {{id}} at the start gives mappers an immediate OSM reference. Keep long per-task copy in task_markdown; keep shared framing (title, update stamps) in the challenge instruction.
5. Point the challenge at remote GeoJSON, then rebuild to update
Do — create/update challenge: set remoteGeoJson to your data URL, or upload line-by-line GeoJSON. A static file (e.g. GitHub Pages, public/… regenerated in CI) works the same as a live API — MapRoulette fetches the URL on rebuild.
Do — Tag Fix layout: include TagDiffWidget in taskWidgetLayout so mappers get approve/reject tag diffs in the task UI.
Do — refresh tasks after source data changes (shell / GitHub Actions step):
CHALLENGE_ID=12345
MAPROULETTE_API_KEY=your-api-key # or ${{ secrets.MAPROULETTE_API_KEY }} in Actions
curl --fail -sS -X PUT \
"https://maproulette.org/api/v2/challenge/${CHALLENGE_ID}/rebuild?removeUnmatched=true&skipSnapshot=true" \
-H "apiKey: ${MAPROULETTE_API_KEY}" \
-H "accept: */*"
Or UI: Challenge → Rebuild (optionally “remove incomplete first”).
Because: create/update only changes challenge metadata; tasks are (re)built from the GeoJSON source. The feed can refresh more often than you rebuild — schedule rebuilds when MR should pick up new tasks. Stable type/id IDs let MapRoulette match existing tasks instead of duplicating. removeUnmatched=true drops incomplete tasks no longer in the feed.
Optional — mixed priority: set properties.priority (e.g. prio1, prio2) and matching challenge priority rules when one challenge mixes high- and low-value fixes.
Checklist
- Line-by-line FeatureCollections with
\x1Eprefix - Same string
type/idonproperties.id,cooperativeWork…data.id, and ideallyfeature.id -
cooperativeWork.meta={ version: 2, type: 1 } -
cooperativeWorkis delta-only; no emptyunsetTags - Challenge instruction starts with
## Kontext {{id}}then{{task_markdown}}(not{{osmIdentifier}}) - Newlines in
task_markdownnormalized with.replaceAll('\n', ' \n') - After data change: rebuild (API or UI), not only challenge PUT
Reference
- Minimal task line: examples/tag-fix-task.ndjson
- Property / Mustache notes: references/task-markdown-and-ids.md