Complex maps through $translate
SNOMED ON FHIR · 18 OCTOBER 2026
Interpreting SNOMED CT Complex and Extended Maps, including the ICD-10 map, with FHIR
Jim Steel, Michael Lawley
CSIRO Australian e-Health Research Centre
OFFICIAL
OFFICIAL
Why this, and why here
CONTEXT
Where it came from
Ontoserver work on the SNOMED CT to ICD-10 map, written up as an ePoster for SNOMED CT Expo 2026 in Sydney next week.
What it is
A terminology server that interprets mapRules inside a standard $translate, and returns the map’s structure when it can’t decide.
Why this group
It picks up the group’s standing item on complex maps in $translate, building on the IG’s implicit ConceptMaps.
Complex maps through $translate · CSIRO AEHRC · 2
OFFICIAL
OFFICIAL
A complex map row is a rule plus a target
RECAP
Bronchitis 32398004 in the SNOMED CT to ICD-10 Extended Map 447562003
Row | mapRule | ICD-10 target | mapCategoryId |
1 / 1 | IFA 445518008 |Age at onset of clinical finding| <= 15.0 years | J20.9 Acute bronchitis, unspecified | context dependent |
1 / 2 | OTHERWISE TRUE | J40 Bronchitis, not specified as acute or chronic | properly classified |
Columns: mapGroup, mapPriority, mapRule, mapAdvice, mapTarget, correlationId, plus mapCategoryId for Extended Maps. Within a group the first satisfied row wins; every group adds a code.
Complex maps through $translate · CSIRO AEHRC · 3
OFFICIAL
OFFICIAL
How these maps get used today
TODAY
Offline tools and tables
Generated from the RF2 files and run locally, e.g. IHTSDO’s snomed-to-icd10-mapper: concept, sex and age in, ICD-10 codes out.
$translate, rules as text
Implicit ConceptMap ?fhir_cm=447562003. Servers such as Snowstorm return every row with its mapRule and mapAdvice as strings.
So each client interprets
Either way the rule grammar is evaluated in each consumer, so the same governed map can give different answers in different places.
Complex maps through $translate · CSIRO AEHRC · 4
OFFICIAL
OFFICIAL
We’ve talked about this before
PREVIOUSLY, IN THIS GROUP
Mar 2023
R5 ConceptMap finalised: property, dependsOn and product clarified, with implicit maps from complex maps in mind.
Jun 2023
Concept Map in R5 page: mapRule and mapAdvice as properties. Left open: how should age and sex be passed in?
Feb 2024
Should the server apply the rules? DK: let the client choose. A Swedish vendor went proprietary for lack of a FHIR standard.
Mar 2024
DK: match.product per extra item, a URI per refset column. KK: R4 products are flat; rules arrive grouped.
Sources: SNOMED on FHIR meeting notes, item “Post R5 Concept Map – support for SNOMED Map features”; “Concept Map in R5” page · 5
OFFICIAL
OFFICIAL
What was raised, and how this work handles it
PICKING UP THE THREADS
Raised in the group | In this work |
How should age and sex be passed in? | dependency: a finding as a Coding, or an observable with a typed value |
The client should be able to apply the rules itself (DK) | Every candidate comes back with a status for each predicate |
match.product per extra item, a URI per refset column (DK) | One product per extra mapGroup; columns as property URIs |
R4 products are flat; rules come back grouped as text (KK) | Structured per row and per group; R4 carried with cross-version extensions |
R4 can carry R5 elements in Parameters (ML) | That is how the R4 response works |
Complex maps through $translate · CSIRO AEHRC · 6
OFFICIAL
OFFICIAL
Let $translate interpret the rules, and hand back the structure when it can’t decide.
THE IDEA
The same request and response shape work whether or not the server can evaluate the mapRule language.
OFFICIAL
OFFICIAL
Open the demo now and follow the examples
PLAY ALONG
Captured mode evaluates real ICD-10 map rows in your browser. Live mode sends the same request to a FHIR terminology server you choose.
Complex maps through $translate · CSIRO AEHRC · 8
OFFICIAL
OFFICIAL
The client decides how much context to send
TWO MODES, ONE MECHANISM
Server resolves
Bronchitis with age at onset = 10 years
→ one match, J20.9, with its age predicate marked satisfied.
Every FHIR client gets the same answer from the same map.
Client decides
Bronchitis with no context
→ two matches, J20.9 and J40, with the age predicate marked unknown.
The client can see that age at onset would settle it.
Play along: ontoserver.csiro.au/demo/complex-maps · 9
OFFICIAL
OFFICIAL
Context goes in as dependency parameters
REQUEST
A finding is present
Satisfies IFA 248152002 |Female|
{"name": "dependency", "part": [
{"name": "attribute", "valueUri":
"http://snomed.info/id/404684003"},
{"name": "value", "valueCoding": {
"system": "http://snomed.info/sct",
"code": "248152002",
"display": "Female"}}
]}
An observable has a value
Satisfies IFA 445518008 |Age at onset| <= 15.0 years
{"name": "dependency", "part": [
{"name": "attribute", "valueUri":
"http://snomed.info/id/445518008"},
{"name": "value", "valueQuantity": {
"value": 10,
"system": "http://unitsofmeasure.org",
"code": "a"}}
]}
Attribute 404684003 |Clinical finding| marks a finding. A supplied finding satisfies IFA X if it is X or a descendant (sex is matched exactly). Units are normalised before comparison.
Play along: ontoserver.csiro.au/demo/complex-maps · 10
OFFICIAL
OFFICIAL
Each predicate has one of three statuses
EVALUATION
satisfied
unsatisfied
unknown
Unknown is not false
Context that wasn’t supplied leaves a predicate unknown. It never rules a row out.
Only a contradiction removes a row
A row drops out only when one of its predicates is unsatisfied.
Only a satisfied row shadows
Within a group, a satisfied row hides lower-priority rows. An unknown row hides nothing.
Groups combine
Each extra mapGroup adds a product. The matches are the combinations of surviving rows.
Play along: ontoserver.csiro.au/demo/complex-maps · 11
OFFICIAL
OFFICIAL
Each match carries the reasoning with it
RESPONSE
match
relationship related-to
concept ICD-10 J20.9
dependsOn
attribute …/id/445518008
value <= 15 years
status unknown
property mapRule
property mapAdvice
property mapCategoryId
product additional-code#2
relationship + concept
The primary group’s target, from mapTarget and correlationId.
dependsOn + status
Every predicate the rows refer to, in the request’s own encoding, with its status.
property
The row’s mapRule, mapAdvice and mapCategoryId, each under its own URI.
product
One for each extra mapGroup: its code, relationship and its own row’s properties.
Play along: ontoserver.csiro.au/demo/complex-maps · 12
OFFICIAL
OFFICIAL
Bronchitis, with nothing else known
EXAMPLE · NO CONTEXT
Play along: ontoserver.csiro.au/demo/complex-maps · 13
OFFICIAL
OFFICIAL
Bronchitis, no context: the J20.9 match
ON THE WIRE · A MATCH
{"name": "match", "part": [
{"name": "relationship", "valueCode": "related-to"},
{"name": "concept", "valueCoding": {"system": "http://hl7.org/fhir/sid/icd-10", "code": "J20.9", …}},
{"name": "dependsOn", "part": [
{"name": "attribute", "valueUri": "http://snomed.info/id/445518008"},
{"name": "value", "valueQuantity": {"value": 15, "comparator": "<=", "unit": "years"}},
{"name": "status", "valueCode": "unknown"}]},
{"name": "property", "part": [{"name": "uri", "valueUri": "http://snomed.info/sct/property/mapRule"},
{"name": "value", "valueString": "IFA 445518008 | Age at onset of … | <= 15.0 years"}]},
{"name": "property", "part": [{"name": "uri", "valueUri": "http://snomed.info/sct/property/mapAdvice"},
{"name": "value", "valueString": "IF AGE AT ONSET … CHOOSE J20.9 | MAP … IS CONTEXT DEPENDENT | …"}]},
{"name": "property", "part": [{"name": "uri", "valueUri": "http://snomed.info/sct/property/mapCategoryId"},
{"name": "value", "valueCoding": {"system": "http://snomed.info/sct", "code": "447639009", …}}]}
]}
The second match is J40 (OTHERWISE TRUE, no dependsOn). “…” marks shortened text.
Play along: ontoserver.csiro.au/demo/complex-maps · 14
OFFICIAL
OFFICIAL
Add age at onset, and the map decides
EXAMPLE · WITH CONTEXT
Play along: ontoserver.csiro.au/demo/complex-maps · 15
OFFICIAL
OFFICIAL
Firework burn: a second code as a product
ON THE WIRE · SEVERAL MAP GROUPS
{"name": "match", "part": [
{"name": "relationship", "valueCode": "related-to"},
{"name": "concept", "valueCoding": {"system": "http://hl7.org/fhir/sid/icd-10", "code": "T30.0", …}},
{"name": "property", … mapRule "TRUE", mapAdvice "ALWAYS T30.0", mapCategoryId 447637006 …},
{"name": "product", "part": [
{"name": "attribute", "valueUri": "http://snomed.info/sct/property/additional-code#2"},
{"name": "value", "valueCoding": {"system": "http://hl7.org/fhir/sid/icd-10", "code": "W39", …}},
{"name": "relationship", "valueCode": "related-to"},
{"name": "property", "part": [{"name": "uri", "valueUri": "http://snomed.info/sct/property/mapRule"},
{"name": "value", "valueString": "TRUE"}]},
{"name": "property", "part": [{"name": "uri", "valueUri": "http://snomed.info/sct/property/mapAdvice"},
{"name": "value", "valueString": "ALWAYS W39 | POSSIBLE REQUIREMENT FOR PLACE OF OCCURRENCE"}]},
{"name": "property", … mapCategoryId 447637006 …}]}
]}
Base $translate doesn’t define relationship or property on a product; they’re this convention’s additions. When a group has several surviving rows, there’s one match per combination.
Play along: ontoserver.csiro.au/demo/complex-maps · 16
OFFICIAL
OFFICIAL
Infertile, intersex: an explicit no map
ON THE WIRE · AN EXPLICIT NO MAP
{"name": "result", "valueBoolean": false},
{"name": "match", "part": [
{"name": "noMap", "valueBoolean": true},
{"name": "property", "part": [
{"name": "uri", "valueUri": "…/property/mapRule"},
{"name": "value", "valueString": "OTHERWISE TRUE"}]},
{"name": "property", "part": [
{"name": "uri", "valueUri": "…/property/mapAdvice"},
{"name": "value", "valueString":
"MAP SOURCE CONCEPT CANNOT BE CLASSIFIED
WITH AVAILABLE DATA"}]},
{"name": "property", "part": [
{"name": "uri", "valueUri": "…/property/mapCategoryId"},
{"name": "value", "valueCoding": {"code": "447638001", …}}]}
]}
Intersex rules out the Female and Male rows, so OTHERWISE TRUE applies, and it has no target.
That comes back as a match with noMap and no concept (FHIR-58697), keeping the rule and reason.
A concept with no rows in the map returns no match at all.
Play along: ontoserver.csiro.au/demo/complex-maps · 17
OFFICIAL
OFFICIAL
Implemented in Ontoserver, across FHIR versions
STATUS
Version | How the response is carried |
R5 | Natively: match.property, dependsOn and product. Only status and the product’s children are additions. |
R4 | Cross-version extensions; correlation uses R4 equivalence codes. Same structure, same evaluation. |
R6 | additionalAttribute declares the dependsOn and product attributes. Status would still need an extension. |
Ontoserver
Complex and Extended map refsets. Available from [release / date TBC].
Not covered (yet)
Reverse translate, the UK’s mapBlock, and free-text mapAdvice.
Play along: ontoserver.csiro.au/demo/complex-maps · 18
OFFICIAL
OFFICIAL
Questions I’d welcome views on
FOR DISCUSSION
Who owns the URIs?
Property URIs for refset columns and a status code system: the URI standard, this IG, or HL7?
Is product the right fit?
It gains relationship and property parts that base $translate doesn’t define. Keep them, or use extensions?
Status in core?
Should per-predicate status be proposed for R6, rather than staying an extension?
Other servers and editions
Would Snowstorm or others align on this shape? What would the UK’s mapBlock need?
Play along: ontoserver.csiro.au/demo/complex-maps · 19
OFFICIAL
OFFICIAL
Thank you
Try the demo: ontoserver.csiro.au/demo/complex-maps
See the ePoster: SNOMED CT Expo 2026, Sydney, 21–23 October
Follow noMap: jira.hl7.org/browse/FHIR-58697
Get in touch: jim.steel@csiro.au
Jim Steel, Michael Lawley · CSIRO Australian e-Health Research Centre
OFFICIAL
OFFICIAL