Subject: [PATCH] Fix #17669, #22096: Replace placeholders in calculations
---
Index: resources/data/validator/numeric.mapcss
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/resources/data/validator/numeric.mapcss b/resources/data/validator/numeric.mapcss
--- a/resources/data/validator/numeric.mapcss	(revision 18732)
+++ b/resources/data/validator/numeric.mapcss	(date 1684421822011)
@@ -67,252 +67,166 @@
   assertNoMatch: "node building:levels=0"; /* valid because there can be building:levels:underground > 0 or roof:levels > 0 */
 }
 
-*[height][height =~ /^[0-9]+(\.[0-9]+)?(( )*(metre|metres|meter|meters|Metre|Metres|Meter|Meters)|m)$/] {
+*[roof:height][roof:height =~ /^0*(\.0*)?( (m|ft))?$/][roof:shape=flat] {
+  throwWarning: tr("{0} is unnecessary for {1}", "{0.tag}", "{2.tag}");
+  group: tr("unnecessary tag");
+  fixRemove: "{0.key}";
+  set zero_roof_height_flat;
+  assertMatch: "node roof:height=0 roof:shape=flat";
+  assertMatch: "node roof:shape=flat roof:height=\"00.00000 ft\" roof:shape=flat";
+  assertNoMatch: "node roof:shape=flat roof:height=2 m roof:shape=flat";
+  assertNoMatch: "node roof:height=0 roof:shape=gabled";
+}
+
+/*********************
+ * Begin Unit checks *
+ *********************/
+/* See https://wiki.openstreetmap.org/wiki/Map_features/Units */
+/* 1. Replace aliases to make the rest of the checks easier to implement */
+/* Distance measurements, note that these should look also match `,` separators to ensure that at least one error is matched */
+/* Meters; Note that we cannot assertMatch "2  m" since we replace the double space with a single space */
+*[height][height            =~ /^(?i)[0-9]+([.,][0-9]+)?( *(metres?|meters?)|m| {2,}m)$/],
+*[roof:height][roof:height  =~ /^(?i)[0-9]+([.,][0-9]+)?( *(metres?|meters?)|m| {2,}m)$/]!.zero_roof_height_flat,
+*[width][width              =~ /^(?i)[0-9]+([.,][0-9]+)?( *(metres?|meters?)|m| {2,}m)$/],
+*[maxwidth][maxwidth        =~ /^(?i)[0-9]+([.,][0-9]+)?( *(metres?|meters?)|m| {2,}m)$/],
+*[min_height][min_height    =~ /^(?i)-?[0-9]+([.,][0-9]+)?( *(metres?|meters?)|m| {2,}m)$/],
+*[maxheight][maxheight      =~ /^(?i)[1-9][0-9]*([.,][0-9]+)?( *(metres?|meters?)|m| {2,}m)$/],
+*[maxlength][maxlength      =~ /^(?i)[1-9][0-9]*([.,][0-9]+)?( *(metres?|meters?)|m| {2,}m)$/] {
   throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  set height_meter_autofix;
-  fixAdd: concat("height=", get(regexp_match("([0-9.]+)( )*(.+)",tag("height")),1)," m");
+  fixAdd: concat("{0.key}", "=", get(regexp_match("([0-9.]+) *.+", "{0.value}"), 1), " m");
   assertMatch: "node height=6.78 meters";
+  assertMatch: "node height=6,78 meters";
   assertMatch: "node height=5  metre";
   assertMatch: "node height=2m";
   assertNoMatch: "node height=2 m";
   assertNoMatch: "node height=5";
+  assertMatch: "node maxheight=6.78 meters";
+  assertMatch: "node maxheight=5  metre";
+  assertMatch: "node maxheight=2m";
+  assertNoMatch: "node maxheight=2 m";
+  assertNoMatch: "node maxheight=5";
+  assertMatch: "node roof:height=6.78 meters";
+  assertMatch: "node roof:height=5  metre";
+  assertMatch: "node roof:height=2m";
+  assertNoMatch: "node roof:height=2 m";
+  assertNoMatch: "node roof:height=5";
+  assertMatch: "node maxlength=6.78 meters";
+  assertMatch: "node maxlength=5  metre";
+  assertMatch: "node maxlength=2m";
+  assertNoMatch: "node maxlength=2 m";
+  assertNoMatch: "node maxlength=5";
+  assertMatch: "node width=6.78 meters";
+  assertMatch: "node width=5  metre";
+  assertMatch: "node width=2m";
+  assertNoMatch: "node width=2 m";
+  assertNoMatch: "node width=5";
+  assertMatch: "node maxwidth=6.78 meters";
+  assertMatch: "node maxwidth=5  metre";
+  assertMatch: "node maxwidth=2m";
+  assertNoMatch: "node maxwidth=2 m";
+  assertNoMatch: "node maxwidth=5";
+  assertMatch: "node min_height=6.78 meters";
+  assertMatch: "node min_height=5  metre";
+  assertMatch: "node min_height=2m";
+  assertNoMatch: "node min_height=2 m";
+  assertNoMatch: "node min_height=5";
 }
-*[height][height =~ /^[0-9]+(\.[0-9]+)?(( )*(foot|Foot|feet|Feet)|ft)$/] {
-  throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  set height_foot_autofix;
-  fixAdd: concat("height=", get(regexp_match("([0-9.]+)( )*(.+)",tag("height")),1)," ft");
+
+/* Foot inches */
+*[height][height            =~ /^(?i)[0-9]+([.,][0-9]+)?( *(foot|feet|ft)| +\')$/],
+*[maxheight][maxheight      =~ /^(?i)[0-9]+([.,][0-9]+)?( *(foot|feet|ft)| +\')$/],
+*[roof:height][roof:height  =~ /^(?i)[0-9]+([.,][0-9]+)?( *(foot|feet|ft)| +\')$/]!.zero_roof_height_flat,
+*[maxlength][maxlength      =~ /^(?i)[0-9]+([.,][0-9]+)?( *(foot|feet|ft)| +\')$/],
+*[width][width              =~ /^(?i)[0-9]+([.,][0-9]+)?( *(foot|feet|ft)| +\')$/],
+*[maxwidth][maxwidth        =~ /^(?i)[0-9]+([.,][0-9]+)?( *(foot|feet|ft)| +\')$/] {
+  throwWarning: tr("unusual value of {0}: use abbreviation for '' for foot and \" for inches, no spaces", "{0.key}");
+  fixAdd: concat("{0.key}", "=", get(regexp_match("([0-9.]+) *.+", "{0.value}"), 1), "'");
   assertMatch: "node height=6.78 foot";
   assertMatch: "node height=5  Feet";
-  assertMatch: "node height=2ft";
-  assertNoMatch: "node height=2 ft";
+  assertMatch: "node height=2 '";
+  assertNoMatch: "node height=2'";
   assertNoMatch: "node height=5";
+  assertMatch: "node maxheight=6.78 foot";
+  assertMatch: "node maxheight=5  Feet";
+  assertMatch: "node maxheight=2 '";
+  assertNoMatch: "node maxheight=2'";
+  assertNoMatch: "node maxheight=5";
+  assertMatch: "node roof:height=6.78 foot";
+  assertMatch: "node roof:height=5  Feet";
+  assertMatch: "node roof:height=2 '";
+  assertNoMatch: "node roof:height=2'";
+  assertNoMatch: "node roof:height=5";
+  assertMatch: "node maxlength=6.78 foot";
+  assertMatch: "node maxlength=5  Feet";
+  assertMatch: "node maxlength=2 '";
+  assertNoMatch: "node maxlength=2'";
+  assertNoMatch: "node maxlength=5";
+  assertMatch: "node width=6.78 foot";
+  assertMatch: "node width=5  Feet";
+  assertMatch: "node width=2 '";
+  assertNoMatch: "node width=2'";
+  assertNoMatch: "node width=5";
+  assertMatch: "node maxwidth=6.78 foot";
+  assertMatch: "node maxwidth=5  Feet";
+  assertMatch: "node maxwidth=2 '";
+  assertNoMatch: "node maxwidth=2'";
+  assertNoMatch: "node maxwidth=5";
 }
-*[height][height =~ /^[0-9]+,[0-9][0-9]?( (m|ft))?$/] {
+
+/* 2. Convert `,` to `.` */
+*[height][height            =~   /^[0-9]+,[0-9][0-9]?( m|\')?$/],
+*[maxheight][maxheight      =~   /^[0-9]+,[0-9][0-9]?( m|\')?$/],
+*[roof:height][roof:height  =~   /^[0-9]+,[0-9][0-9]?( m|\')?$/],
+*[maxlength][maxlength      =~   /^[0-9]+,[0-9][0-9]?( m|\')?$/],
+*[width][width              =~   /^[0-9]+,[0-9][0-9]?( m|\')?$/],
+*[maxwidth][maxwidth        =~   /^[0-9]+,[0-9][0-9]?( m|\')?$/],
+*[min_height][min_height    =~ /^-?[0-9]+,[0-9][0-9]?( m|\')?$/],
+*[maxaxleload][maxaxleload  =~   /^[0-9]+,[0-9][0-9]?( (t|kg|st|lbs))?$/],
+*[maxweight][maxweight      =~   /^[0-9]+,[0-9][0-9]?( (t|kg|st|lbs))?$/],
+*[distance][distance        =~   /^[0-9]+,[0-9][0-9]?( (m|km|mi|nmi))?$/] {
   throwWarning: tr("unusual value of {0}: use . instead of , as decimal separator", "{0.key}");
-  fixAdd: concat("height=", replace(tag("height"), ",", "."));
-  set height_separator_autofix;
+  fixAdd: concat("{0.key}", "=", replace(tag("{0.key}"), ",", "."));
+  set separator_autofix;
   assertMatch: "node height=5,5";
   assertMatch: "node height=12,00";
-  assertMatch: "node height=12,5 ft";
+  assertMatch: "node height=12,5'";
   assertNoMatch: "node height=12,000";
   assertNoMatch: "node height=3,50,5";
   assertNoMatch: "node height=3.5";
   assertNoMatch: "node height=4";
-}
-
-*[maxheight][maxheight =~ /^[1-9][0-9]*(\.[0-9]+)?(( )*(metre|metres|meter|meters|Metre|Metres|Meter|Meters)|m)$/] {
-  throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  set maxheight_meter_autofix;
-  fixAdd: concat("maxheight=", get(regexp_match("([0-9.]+)( )*(.+)",tag("maxheight")),1)," m");
-  assertMatch: "node maxheight=6.78 meters";
-  assertMatch: "node maxheight=5  metre";
-  assertMatch: "node maxheight=2m";
-  assertNoMatch: "node maxheight=2 m";
-  assertNoMatch: "node maxheight=5";
-}
-*[maxheight][maxheight =~ /^[0-9]+(\.[0-9]+)?(( )*(foot|Foot|feet|Feet)|ft)$/] {
-  throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  set maxheight_foot_autofix;
-  fixAdd: concat("maxheight=", get(regexp_match("([0-9.]+)( )*(.+)",tag("maxheight")),1)," ft");
-  assertMatch: "node maxheight=6.78 foot";
-  assertMatch: "node maxheight=5  Feet";
-  assertMatch: "node maxheight=2ft";
-  assertNoMatch: "node maxheight=2 ft";
-  assertNoMatch: "node maxheight=5";
-}
-*[maxheight][maxheight =~ /^[0-9]+,[0-9][0-9]?( (m|ft))?$/] {
-  throwWarning: tr("unusual value of {0}: use . instead of , as decimal separator", "{0.key}");
-  fixAdd: concat("maxheight=", replace(tag("maxheight"), ",", "."));
-  set maxheight_separator_autofix;
   assertMatch: "node maxheight=5,5";
   assertMatch: "node maxheight=12,00";
-  assertMatch: "node maxheight=12,5 ft";
+  assertMatch: "node maxheight=12,5'";
   assertNoMatch: "node maxheight=12,000";
   assertNoMatch: "node maxheight=3,50,5";
   assertNoMatch: "node maxheight=3.5";
   assertNoMatch: "node maxheight=4";
-}
-
-*[roof:height][roof:height =~ /^0*(\.0*)?( (m|ft))?$/][roof:shape=flat] {
-  throwWarning: tr("{0} is unnecessary for {1}", "{0.tag}", "{2.tag}");
-  group: tr("unnecessary tag");
-  fixRemove: "{0.key}";
-  set zero_roof_height_flat;
-  assertMatch: "node roof:height=0 roof:shape=flat";
-  assertMatch: "node roof:shape=flat roof:height=\"00.00000 ft\" roof:shape=flat";
-  assertNoMatch: "node roof:shape=flat roof:height=2 m roof:shape=flat";
-  assertNoMatch: "node roof:height=0 roof:shape=gabled";
-}
-*[roof:height][roof:height =~ /^[0-9]+(\.[0-9]+)?(( )*(metre|metres|meter|meters|Metre|Metres|Meter|Meters)|m)$/]!.zero_roof_height_flat {
-  throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  set roof_height_meter_autofix;
-  fixAdd: concat("roof:height=", get(regexp_match("([0-9.]+)( )*(.+)",tag("roof:height")),1)," m");
-  assertMatch: "node roof:height=6.78 meters";
-  assertMatch: "node roof:height=5  metre";
-  assertMatch: "node roof:height=2m";
-  assertNoMatch: "node roof:height=2 m";
-  assertNoMatch: "node roof:height=5";
-}
-*[roof:height][roof:height =~ /^[0-9]+(\.[0-9]+)?(( )*(foot|Foot|feet|Feet)|ft)$/]!.zero_roof_height_flat {
-  throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  set roof_height_foot_autofix;
-  fixAdd: concat("roof:height=", get(regexp_match("([0-9.]+)( )*(.+)",tag("roof:height")),1)," ft");
-  assertMatch: "node roof:height=6.78 foot";
-  assertMatch: "node roof:height=5  Feet";
-  assertMatch: "node roof:height=2ft";
-  assertNoMatch: "node roof:height=2 ft";
-  assertNoMatch: "node roof:height=5";
-}
-*[roof:height][roof:height =~ /^[0-9]+,[0-9][0-9]?( (m|ft))?$/] {
-  throwWarning: tr("unusual value of {0}: use . instead of , as decimal separator", "{0.key}");
-  fixAdd: concat("roof:height=", replace(tag("roof:height"), ",", "."));
-  set roof_height_separator_autofix;
   assertMatch: "node roof:height=5,5";
   assertMatch: "node roof:height=12,00";
-  assertMatch: "node roof:height=12,5 ft";
+  assertMatch: "node roof:height=12,5'";
   assertNoMatch: "node roof:height=12,000";
   assertNoMatch: "node roof:height=3,50,5";
   assertNoMatch: "node roof:height=3.5";
   assertNoMatch: "node roof:height=4";
-}
-
-*[maxlength][maxlength =~ /^[1-9][0-9]*(\.[0-9]+)?(( )*(metre|metres|meter|meters|Metre|Metres|Meter|Meters)|m)$/] {
-  throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  set maxlength_meter_autofix;
-  fixAdd: concat("maxlength=", get(regexp_match("([0-9.]+)( )*(.+)",tag("maxlength")),1)," m");
-  assertMatch: "node maxlength=6.78 meters";
-  assertMatch: "node maxlength=5  metre";
-  assertMatch: "node maxlength=2m";
-  assertNoMatch: "node maxlength=2 m";
-  assertNoMatch: "node maxlength=5";
-}
-*[maxlength][maxlength =~ /^[0-9]+(\.[0-9]+)?(( )*(foot|Foot|feet|Feet)|ft)$/] {
-  throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  set maxlength_foot_autofix;
-  fixAdd: concat("maxlength=", get(regexp_match("([0-9.]+)( )*(.+)",tag("maxlength")),1)," ft");
-  assertMatch: "node maxlength=6.78 foot";
-  assertMatch: "node maxlength=5  Feet";
-  assertMatch: "node maxlength=2ft";
-  assertNoMatch: "node maxlength=2 ft";
-  assertNoMatch: "node maxlength=5";
-}
-*[maxlength][maxlength =~ /^[0-9]+,[0-9][0-9]?( (m|ft))?$/] {
-  throwWarning: tr("unusual value of {0}: use . instead of , as decimal separator", "{0.key}");
-  fixAdd: concat("maxlength=", replace(tag("maxlength"), ",", "."));
-  set maxlength_separator_autofix;
   assertMatch: "node maxlength=5,5";
   assertMatch: "node maxlength=12,00";
-  assertMatch: "node maxlength=12,5 ft";
+  assertMatch: "node maxlength=12,5'";
   assertNoMatch: "node maxlength=12,000";
   assertNoMatch: "node maxlength=3,50,5";
   assertNoMatch: "node maxlength=3.5";
   assertNoMatch: "node maxlength=4";
-}
-
-*[width][width =~ /^[0-9]+(\.[0-9]+)?(( )*(metre|metres|meter|meters|Metre|Metres|Meter|Meters)|m)$/] {
-  throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  set width_meter_autofix;
-  fixAdd: concat("width=", get(regexp_match("([0-9.]+)( )*(.+)",tag("width")),1)," m");
-  assertMatch: "node width=6.78 meters";
-  assertMatch: "node width=5  metre";
-  assertMatch: "node width=2m";
-  assertNoMatch: "node width=2 m";
-  assertNoMatch: "node width=5";
-}
-*[width][width =~ /^[0-9]+(\.[0-9]+)?(( )*(foot|Foot|feet|Feet)|ft)$/] {
-  throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  set width_foot_autofix;
-  fixAdd: concat("width=", get(regexp_match("([0-9.]+)( )*(.+)",tag("width")),1)," ft");
-  assertMatch: "node width=6.78 foot";
-  assertMatch: "node width=5  Feet";
-  assertMatch: "node width=2ft";
-  assertNoMatch: "node width=2 ft";
-  assertNoMatch: "node width=5";
-}
-*[width][width =~ /^[0-9]+,[0-9][0-9]?( (m|ft))?$/] {
-  throwWarning: tr("unusual value of {0}: use . instead of , as decimal separator", "{0.key}");
-  fixAdd: concat("width=", replace(tag("width"), ",", "."));
-  set width_separator_autofix;
   assertMatch: "node width=5,5";
   assertMatch: "node width=12,00";
   assertNoMatch: "node width=12,000";
   assertNoMatch: "node width=3,50,5";
   assertNoMatch: "node width=3.5";
   assertNoMatch: "node width=4";
-}
-
-*[maxwidth][maxwidth=~ /^[0-9]+(\.[0-9]+)?(( )*(metre|metres|meter|meters|Metre|Metres|Meter|Meters)|m)$/] {
-  throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  set maxwidth_meter_autofix;
-  fixAdd: concat("maxwidth=", get(regexp_match("([0-9.]+)( )*(.+)",tag("maxwidth")),1)," m");
-  assertMatch: "node maxwidth=6.78 meters";
-  assertMatch: "node maxwidth=5  metre";
-  assertMatch: "node maxwidth=2m";
-  assertNoMatch: "node maxwidth=2 m";
-  assertNoMatch: "node maxwidth=5";
-}
-*[maxwidth][maxwidth =~ /^[0-9]+(\.[0-9]+)?(( )*(foot|Foot|feet|Feet)|ft)$/] {
-  throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  set maxwidth_foot_autofix;
-  fixAdd: concat("maxwidth=", get(regexp_match("([0-9.]+)( )*(.+)",tag("maxwidth")),1)," ft");
-  assertMatch: "node maxwidth=6.78 foot";
-  assertMatch: "node maxwidth=5  Feet";
-  assertMatch: "node maxwidth=2ft";
-  assertNoMatch: "node maxwidth=2 ft";
-  assertNoMatch: "node maxwidth=5";
-}
-*[maxwidth][maxwidth =~ /^[0-9]+,[0-9][0-9]?( (m|ft))?$/] {
-  throwWarning: tr("unusual value of {0}: use . instead of , as decimal separator", "{0.key}");
-  fixAdd: concat("maxwidth=", replace(tag("maxwidth"), ",", "."));
-  set maxwidth_separator_autofix;
   assertMatch: "node maxwidth=5,5";
   assertMatch: "node maxwidth=12,00";
   assertNoMatch: "node maxwidth=12,000";
   assertNoMatch: "node maxwidth=3,50,5";
   assertNoMatch: "node maxwidth=3.5";
   assertNoMatch: "node maxwidth=4";
-}
-
-*[height     ][height      !~ /^(([0-9]+(\.[0-9]+)?( (m|ft))?)|([1-9][0-9]*\'((10|11|[0-9])((\.[0-9]+)?)\")?))$/]!.height_separator_autofix!.height_meter_autofix!.height_foot_autofix,
-*[maxheight  ][maxheight   !~ /^(([1-9][0-9]*(\.[0-9]+)?( (m|ft))?)|([0-9]+\'(([0-9]|10|11)(\.[0-9]*)?\")?)|none|default|below_default)$/]!.maxheight_separator_autofix!.maxheight_meter_autofix!.maxheight_foot_autofix,
-*[roof:height][roof:height !~ /^(([0-9]+(\.[0-9]+)?( (m|ft))?)|([1-9][0-9]*\'((10|11|[0-9])((\.[0-9]+)?)\")?))$/]!.roof_height_separator_autofix!.roof_height_meter_autofix!.roof_height_foot_autofix!.zero_roof_height_flat,
-*[maxlength  ][maxlength   !~ /^(([1-9][0-9]*(\.[0-9]+)?( (m|ft))?)|([0-9]+\'(([0-9]|10|11)(\.[0-9]*)?\")?)|none|default|below_default)$/]!.maxlength_separator_autofix!.maxlength_meter_autofix!.maxlength_foot_autofix,
-*[width      ][width       !~ /^(([0-9]+(\.[0-9]+)?( (m|ft))?)|([0-9]+\'([0-9]+(\.[0-9]+)?\")?))$/]!.width_separator_autofix!.width_meter_autofix!.width_foot_autofix,
-*[maxwidth   ][maxwidth    !~ /^(([0-9]+(\.[0-9]+)?( (m|ft))?)|([0-9]+\'([0-9]+(\.[0-9]+)?\")?))$/]!.maxwidth_separator_autofix!.maxwidth_meter_autofix!.maxwidth_foot_autofix {
-  throwWarning: tr("unusual value of {0}: {1} is default; only positive values; point is decimal separator; if units, put space then unit", "{0.key}", tr("meters"));
-  assertMatch: "node height=medium";
-  assertMatch: "node maxheight=-5";
-  assertMatch: "node maxlength=0";
-  assertMatch: "node maxlength=10'13\"";
-  assertMatch: "node width=10'2.\"";
-  assertMatch: "node maxheight=\"2. m\"";
-  assertMatch: "node height=\"12. m\"";
-  assertNoMatch: "node height=6.78 meters";
-  assertNoMatch: "node height=5  metre";
-  assertNoMatch: "node height=2m";
-  assertNoMatch: "node height=3";
-  assertNoMatch: "node height=2.22 m";
-  assertNoMatch: "node height=7.8";
-  assertNoMatch: "node maxwidth=7 ft";
-  assertNoMatch: "node height=22'";
-  assertNoMatch: "node width=10'5\"";
-  assertNoMatch: "node width=10'";
-}
-
-*[min_height][min_height =~ /^-?[0-9]+(\.[0-9]+)?(( )*(metre|metres|meter|meters|Metre|Metres|Meter|Meters)|m)$/] {
-  throwWarning: tr("unusual value of {0}: use abbreviation for unit and space between value and unit", "{0.key}");
-  fixAdd: concat("min_height=", get(regexp_match("(-?[0-9.]+)( )*(.+)",tag("min_height")),1)," m");
-  set min_height_meter_autofix;
-  assertMatch: "node min_height=6.78 meters";
-  assertMatch: "node min_height=5  metre";
-  assertMatch: "node min_height=2m";
-  assertNoMatch: "node min_height=2 m";
-  assertNoMatch: "node min_height=5";
-}
-*[min_height][min_height =~ /^-?[0-9]+,[0-9][0-9]?( m|\')?$/] {
-  throwWarning: tr("unusual value of {0}: use . instead of , as decimal separator", "{0.key}");
-  fixAdd: concat("min_height=", replace(tag("min_height"), ",", "."));
-  set min_height_separator_autofix;
   assertMatch: "node min_height=5,5";
   assertMatch: "node min_height=12,00";
   assertMatch: "node min_height=12,5'";
@@ -320,39 +234,59 @@
   assertNoMatch: "node min_height=3,50,5";
   assertNoMatch: "node min_height=3.5";
   assertNoMatch: "node min_height=4";
-}
-*[min_height ][min_height  !~ /^(-?([0-9]+(\.[0-9]+)?( m)?)|(-?[1-9][0-9]*\'((10|11|[0-9])((\.[0-9]+)?)\")?))$/]!.min_height_separator_autofix!.min_height_meter_autofix!.min_height_foot_autofix {
-  throwWarning: tr("unusual value of {0}: {1} is default; point is decimal separator; if units, put space then unit", "{0.key}", tr("meters"));
-  assertMatch: "node min_height=\"12. m\"";
-  assertNoMatch: "node min_height=-5";
-}
-
-*[maxaxleload][maxaxleload =~ /^[0-9]+,[0-9][0-9]?( (t|kg|st|lbs))?$/] {
-  throwWarning: tr("unusual value of {0}: use . instead of , as decimal separator", "{0.key}");
-  fixAdd: concat("maxaxleload=", replace(tag("maxaxleload"), ",", "."));
-  set maxaxleload_separator_autofix;
   assertMatch: "node maxaxleload=5,5";
   assertMatch: "node maxaxleload=12,00";
   assertNoMatch: "node maxaxleload=12,000";
   assertNoMatch: "node maxaxleload=3,50,5";
   assertNoMatch: "node maxaxleload=3.5";
   assertNoMatch: "node maxaxleload=4";
-}
-
-*[maxweight][maxweight =~ /^[0-9]+,[0-9][0-9]?( (t|kg|st|lbs))?$/] {
-  throwWarning: tr("unusual value of {0}: use . instead of , as decimal separator", "{0.key}");
-  fixAdd: concat("maxweight=", replace(tag("maxweight"), ",", "."));
-  set maxweight_separator_autofix;
   assertMatch: "node maxweight=5,5";
   assertMatch: "node maxweight=12,00";
   assertNoMatch: "node maxweight=12,000";
   assertNoMatch: "node maxweight=3,50,5";
   assertNoMatch: "node maxweight=3.5";
   assertNoMatch: "node maxweight=4";
+  assertMatch: "node distance=5,5";
+  assertMatch: "node distance=12,00";
+  assertNoMatch: "node distance=12,000";
+  assertNoMatch: "node distance=3,50,5";
+  assertNoMatch: "node distance=3.5";
+  assertNoMatch: "node distance=4";
+}
+/* 3. Convert to the default unit for usage later. Use the tag prefixed with _. */
+/* 4. Start doing comparison checks. */
+
+*[height     ][height      !~ /^(([0-9]+(\.[0-9]+)?( m)?)|([1-9][0-9]*\'((10|11|[0-9])((\.[0-9]+)?)\")?))$/],
+*[maxheight  ][maxheight   !~ /^(([1-9][0-9]*(\.[0-9]+)?( m)?)|([0-9]+\'(([0-9]|10|11)(\.[0-9]*)?\")?)|none|default|below_default)$/],
+*[min_height ][min_height  !~ /^(-?([0-9]+(\.[0-9]+)?( m)?)|(-?[1-9][0-9]*\'((10|11|[0-9])((\.[0-9]+)?)\")?))$/],
+*[roof:height][roof:height !~ /^(([0-9]+(\.[0-9]+)?( m)?)|([1-9][0-9]*\'((10|11|[0-9])((\.[0-9]+)?)\")?))$/]!.zero_roof_height_flat,
+*[maxlength  ][maxlength   !~ /^(([1-9][0-9]*(\.[0-9]+)?( m)?)|([0-9]+\'(([0-9]|10|11)(\.[0-9]*)?\")?)|none|default|below_default)$/],
+*[width      ][width       !~ /^(([0-9]+(\.[0-9]+)?( m)?)|([0-9]+\'([0-9]+(\.[0-9]+)?\")?))$/],
+*[maxwidth   ][maxwidth    !~ /^(([0-9]+(\.[0-9]+)?( m)?)|([0-9]+\'([0-9]+(\.[0-9]+)?\")?))$/] {
+  throwWarning: tr("unusual value of {0}: meters is default; only positive values; point is decimal separator; if units, put space then unit", "{0.key}");
+  assertMatch: "node height=medium";
+  assertMatch: "node maxheight=-5";
+  assertMatch: "node maxlength=0";
+  assertMatch: "node maxlength=10'13\"";
+  assertMatch: "node width=10'2.\"";
+  assertMatch: "node maxheight=\"2. m\"";
+  assertMatch: "node height=\"12. m\"";
+  assertNoMatch: "node height=6.78 m";
+  assertNoMatch: "node height=5  m";
+  assertNoMatch: "node height=2 m";
+  assertNoMatch: "node height=3";
+  assertNoMatch: "node height=2.22 m";
+  assertNoMatch: "node height=7.8";
+  assertMatch: "node min_height=\"12. m\"";
+  assertNoMatch: "node min_height=-5";
+  assertNoMatch: "node maxwidth=7'";
+  assertNoMatch: "node height=22'";
+  assertNoMatch: "node width=10'5\"";
+  assertNoMatch: "node width=10'";
 }
 
-*[maxaxleload][maxaxleload !~ /^([0-9]+(\.[0-9]+)?( (t|kg|st|lbs))?)$/]!.maxaxleload_separator_autofix,
-*[maxweight][maxweight !~ /^([0-9]+(\.[0-9]+)?( (t|kg|st|lbs))?)$/]!.maxweight_separator_autofix {
+*[maxaxleload][maxaxleload !~ /^([0-9]+(\.[0-9]+)?( (t|kg|st|lbs))?)$/],
+*[maxweight][maxweight !~ /^([0-9]+(\.[0-9]+)?( (t|kg|st|lbs))?)$/] {
   throwWarning: tr("unusual value of {0}: {1} is default; only positive values; point is decimal separator; if units, put space then unit", "{0.key}", tr("tonne"));
   assertMatch: "node maxaxleload=something";
   assertMatch: "node maxweight=-5";
@@ -379,18 +313,7 @@
   assertNoMatch: "way maxspeed=variable";
 }
 
-*[distance][distance =~ /^[0-9]+,[0-9][0-9]?( (m|km|mi|nmi))?$/] {
-  throwWarning: tr("unusual value of {0}: use . instead of , as decimal separator", "{0.key}");
-  fixAdd: concat("distance=", replace(tag("distance"), ",", "."));
-  set distance_separator_autofix;
-  assertMatch: "node distance=5,5";
-  assertMatch: "node distance=12,00";
-  assertNoMatch: "node distance=12,000";
-  assertNoMatch: "node distance=3,50,5";
-  assertNoMatch: "node distance=3.5";
-  assertNoMatch: "node distance=4";
-}
-*[distance][distance !~ /^(([0-9]+(\.[0-9]+)?( (m|km|mi|nmi))?)|([0-9]+\'([0-9]+(\.[0-9]+)?\")?))$/]!.distance_separator_autofix {
+*[distance][distance !~ /^(([0-9]+(\.[0-9]+)?( (m|km|mi|nmi))?)|([0-9]+\'([0-9]+(\.[0-9]+)?\")?))$/] {
   throwWarning: tr("unusual value of {0}: {1} is default; only positive values; point is decimal separator; if units, put space then unit", "{0.key}", tr("kilometers"));
   assertMatch: "way distance=something";
   assertMatch: "way distance=-5";
@@ -421,6 +344,10 @@
   assertNoMatch: "way frequency=123.5 MHz";
 }
 
+/*******************
+ * End Unit checks *
+ *******************/
+
 way[gauge][gauge      =~ /^(broad|standard|narrow)$/],
 relation[gauge][gauge =~ /^(broad|standard|narrow)$/] {
   throwWarning: tr("imprecise value of {0}", "{0.tag}");
@@ -523,7 +450,7 @@
   assertMatch: "node direction=45-100;190-250;300-";
   assertNoMatch: "node direction=45-100;190-250;300";
   assertNoMatch: "node direction=90;270";
-  assertNoMatch: "node direction=up"; 
+  assertNoMatch: "node direction=up";
   assertNoMatch: "node direction=down"; /* up/down are replaced by incline tag, has separate warning */
   assertMatch: "node direction=-10";
   assertNoMatch: "node direction=0";
Index: src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java
--- a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java	(revision 18732)
+++ b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java	(date 1684421822046)
@@ -231,9 +231,10 @@
                 }
                 final Selector selector = check.whichSelectorMatchesEnvironment(env);
                 if (selector != null) {
-                    check.rule.declaration.execute(env);
+                    final Environment envWithSelector = env.withSelector(selector);
+                    check.rule.declaration.execute(envWithSelector);
                     if (!ignoreError && !check.errors.isEmpty()) {
-                        r.addAll(check.getErrorsForPrimitive(p, selector, env, new MapCSSTagCheckerAndRule(check.rule)));
+                        r.addAll(check.getErrorsForPrimitive(p, selector, envWithSelector, new MapCSSTagCheckerAndRule(check.rule)));
                     }
                 }
             }
Index: src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerAsserts.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerAsserts.java b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerAsserts.java
--- a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerAsserts.java	(revision 18732)
+++ b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerAsserts.java	(date 1684421822140)
@@ -74,8 +74,8 @@
                 // Check that autofix works as expected
                 Command fix = check.fixPrimitive(p);
                 if (fix != null && fix.executeCommand() && !MapCSSTagChecker.getErrorsForPrimitive(p, true, checksToRun).isEmpty()) {
-                    assertionConsumer.accept(MessageFormat.format("Autofix does not work for test ''{0}'' (i.e., {1})",
-                            check.getMessage(p), check.rule.selectors));
+                    assertionConsumer.accept(MessageFormat.format("Autofix does not work for test ''{0}'' (i.e., {1}). Failing test: {2}",
+                            check.getMessage(p), check.rule.selectors, i.getKey()));
                 }
             }
             ds.removePrimitive(p);
Index: src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerFixCommand.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerFixCommand.java b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerFixCommand.java
--- a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerFixCommand.java	(revision 18732)
+++ b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerFixCommand.java	(date 1684421822235)
@@ -48,7 +48,7 @@
     static String evaluateObject(final Object obj, final OsmPrimitive p, final Selector matchingSelector) {
         final String s;
         if (obj instanceof Expression) {
-            s = (String) ((Expression) obj).evaluate(new Environment(p));
+            s = (String) ((Expression) obj).evaluate(new Environment(p).withSelector(matchingSelector));
         } else if (obj instanceof String) {
             s = (String) obj;
         } else {
Index: src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java
--- a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java	(revision 18732)
+++ b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java	(date 1684422588710)
@@ -17,8 +17,6 @@
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import org.openstreetmap.josm.command.Command;
@@ -26,7 +24,6 @@
 import org.openstreetmap.josm.command.SequenceCommand;
 import org.openstreetmap.josm.data.osm.IPrimitive;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Tag;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.WaySegment;
 import org.openstreetmap.josm.data.validation.Severity;
@@ -34,12 +31,13 @@
 import org.openstreetmap.josm.data.validation.TestError;
 import org.openstreetmap.josm.gui.mappaint.Environment;
 import org.openstreetmap.josm.gui.mappaint.Keyword;
+import org.openstreetmap.josm.gui.mappaint.MultiCascade;
 import org.openstreetmap.josm.gui.mappaint.mapcss.Condition;
-import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.TagCondition;
 import org.openstreetmap.josm.gui.mappaint.mapcss.Expression;
 import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction;
 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule;
 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
+import org.openstreetmap.josm.gui.mappaint.mapcss.PlaceholderExpression;
 import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
 import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
 import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
@@ -214,7 +212,7 @@
     }
 
     Selector whichSelectorMatchesPrimitive(OsmPrimitive primitive) {
-        return whichSelectorMatchesEnvironment(new Environment(primitive));
+        return whichSelectorMatchesEnvironment(new Environment(primitive, new MultiCascade(), Environment.DEFAULT_LAYER, null));
     }
 
     Selector whichSelectorMatchesEnvironment(Environment env) {
@@ -224,37 +222,6 @@
                 .orElse(null);
     }
 
-    /**
-     * Determines the {@code index}-th key/value/tag (depending on {@code type}) of the
-     * {@link org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector}.
-     *
-     * @param matchingSelector matching selector
-     * @param index            index
-     * @param type             selector type ("key", "value" or "tag")
-     * @param p                OSM primitive
-     * @return argument value, can be {@code null}
-     */
-    static String determineArgument(Selector.GeneralSelector matchingSelector, int index, String type, OsmPrimitive p) {
-        try {
-            final Condition c = matchingSelector.getConditions().get(index);
-            final Tag tag = c instanceof TagCondition
-                    ? ((TagCondition) c).asTag(p)
-                    : null;
-            if (tag == null) {
-                return null;
-            } else if ("key".equals(type)) {
-                return tag.getKey();
-            } else if ("value".equals(type)) {
-                return tag.getValue();
-            } else if ("tag".equals(type)) {
-                return tag.toString();
-            }
-        } catch (IndexOutOfBoundsException ignore) {
-            Logging.debug(ignore);
-        }
-        return null;
-    }
-
     /**
      * Replaces occurrences of <code>{i.key}</code>, <code>{i.value}</code>, <code>{i.tag}</code> in {@code s} by the corresponding
      * key/value/tag of the {@code index}-th {@link Condition} of {@code matchingSelector}.
@@ -265,25 +232,7 @@
      * @return string with arguments inserted
      */
     static String insertArguments(Selector matchingSelector, String s, OsmPrimitive p) {
-        if (s != null && matchingSelector instanceof Selector.ChildOrParentSelector) {
-            return insertArguments(((Selector.ChildOrParentSelector) matchingSelector).right, s, p);
-        } else if (s == null || !(matchingSelector instanceof Selector.GeneralSelector)) {
-            return s;
-        }
-        final Matcher m = Pattern.compile("\\{(\\d+)\\.(key|value|tag)\\}").matcher(s);
-        final StringBuffer sb = new StringBuffer();
-        while (m.find()) {
-            final String argument = determineArgument((Selector.GeneralSelector) matchingSelector,
-                    Integer.parseInt(m.group(1)), m.group(2), p);
-            try {
-                // Perform replacement with null-safe + regex-safe handling
-                m.appendReplacement(sb, String.valueOf(argument).replace("^(", "").replace(")$", ""));
-            } catch (IndexOutOfBoundsException | IllegalArgumentException e) {
-                Logging.log(Logging.LEVEL_ERROR, tr("Unable to replace argument {0} in {1}: {2}", argument, sb, e.getMessage()), e);
-            }
-        }
-        m.appendTail(sb);
-        return sb.toString();
+        return PlaceholderExpression.insertArguments(matchingSelector, s, p);
     }
 
     /**
@@ -328,7 +277,7 @@
             final Object val = errors.keySet().iterator().next().val;
             return String.valueOf(
                     val instanceof Expression
-                            ? ((Expression) val).evaluate(new Environment(p))
+                            ? ((Expression) val).evaluate(new Environment(p).withSelector(p == null ? null : whichSelectorMatchesPrimitive(p)))
                             : val
             );
         }
Index: src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSParser.jj
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSParser.jj b/src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSParser.jj
--- a/src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSParser.jj	(revision 18732)
+++ b/src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSParser.jj	(date 1684421822501)
@@ -31,6 +31,7 @@
 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException;
 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule;
 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
+import org.openstreetmap.josm.gui.mappaint.mapcss.PlaceholderExpression;
 import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
 import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.ChildOrParentSelector;
 import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector;
@@ -1052,6 +1053,9 @@
         { 
             if (lit == null)
                 return NullExpression.INSTANCE;
+            else if (lit instanceof String && PlaceholderExpression.PATTERN_PLACEHOLDER.matcher((String) lit).find()) {
+                return new PlaceholderExpression((String) lit);
+            }
             return new LiteralExpression(lit);
         }
     |
Index: src/org/openstreetmap/josm/gui/mappaint/mapcss/PlaceholderExpression.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/gui/mappaint/mapcss/PlaceholderExpression.java b/src/org/openstreetmap/josm/gui/mappaint/mapcss/PlaceholderExpression.java
new file mode 100644
--- /dev/null	(date 1684421822504)
+++ b/src/org/openstreetmap/josm/gui/mappaint/mapcss/PlaceholderExpression.java	(date 1684421822504)
@@ -0,0 +1,109 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.mappaint.mapcss;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.Tagged;
+import org.openstreetmap.josm.gui.mappaint.Environment;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Used for expressions that contain placeholders
+ * @since xxx
+ */
+public final class PlaceholderExpression implements Expression {
+    /**
+     * The regex used for pattern replacement
+     */
+    public static final Pattern PATTERN_PLACEHOLDER = Pattern.compile("\\{(\\d+)\\.(key|value|tag)}");
+    private final String placeholder;
+
+    /**
+     * Constructs a new {@link PlaceholderExpression}.
+     * @param placeholder The placeholder expression
+     */
+    public PlaceholderExpression(String placeholder) {
+        CheckParameterUtil.ensureParameterNotNull(placeholder);
+        this.placeholder = placeholder.intern();
+    }
+
+    @Override
+    public Object evaluate(Environment env) {
+        if (env.selector() == null) {
+            return placeholder;
+        }
+        return insertArguments(env.selector(), placeholder, env.osm);
+    }
+
+    /**
+     * Replaces occurrences of <code>{i.key}</code>, <code>{i.value}</code>, <code>{i.tag}</code> in {@code s} by the corresponding
+     * key/value/tag of the {@code index}-th {@link Condition} of {@code matchingSelector}.
+     *
+     * @param matchingSelector matching selector
+     * @param s                any string
+     * @param p                OSM primitive
+     * @return string with arguments inserted
+     */
+    public static String insertArguments(Selector matchingSelector, String s, Tagged p) {
+        if (s != null && matchingSelector instanceof Selector.ChildOrParentSelector) {
+            return insertArguments(((Selector.ChildOrParentSelector) matchingSelector).right, s, p);
+        } else if (s == null || !(matchingSelector instanceof Selector.GeneralSelector)) {
+            return s;
+        }
+        final Matcher m = PATTERN_PLACEHOLDER.matcher(s);
+        final StringBuffer sb = new StringBuffer();
+        while (m.find()) {
+            final String argument = determineArgument(matchingSelector,
+                    Integer.parseInt(m.group(1)), m.group(2), p);
+            try {
+                // Perform replacement with null-safe + regex-safe handling
+                m.appendReplacement(sb, String.valueOf(argument).replace("^(", "").replace(")$", ""));
+            } catch (IndexOutOfBoundsException | IllegalArgumentException e) {
+                Logging.log(Logging.LEVEL_ERROR, tr("Unable to replace argument {0} in {1}: {2}", argument, sb, e.getMessage()), e);
+            }
+        }
+        m.appendTail(sb);
+        return sb.toString();
+    }
+
+    /**
+     * Determines the {@code index}-th key/value/tag (depending on {@code type}) of the
+     * {@link org.openstreetmap.josm.gui.mappaint.mapcss.Selector}.
+     *
+     * @param matchingSelector matching selector
+     * @param index            index
+     * @param type             selector type ("key", "value" or "tag")
+     * @param p                OSM primitive
+     * @return argument value, can be {@code null}
+     */
+    private static String determineArgument(Selector matchingSelector, int index, String type, Tagged p) {
+        try {
+            final Condition c = matchingSelector.getConditions().get(index);
+            final Tag tag = c instanceof Condition.TagCondition
+                    ? ((Condition.TagCondition) c).asTag(p)
+                    : null;
+            if (tag == null) {
+                return null;
+            } else if ("key".equals(type)) {
+                return tag.getKey();
+            } else if ("value".equals(type)) {
+                return tag.getValue();
+            } else if ("tag".equals(type)) {
+                return tag.toString();
+            }
+        } catch (IndexOutOfBoundsException ioobe) {
+            Logging.debug(ioobe);
+        }
+        return null;
+    }
+
+    @Override
+    public String toString() {
+        return '<' + placeholder + '>';
+    }
+}
Index: src/org/openstreetmap/josm/gui/mappaint/Environment.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/gui/mappaint/Environment.java b/src/org/openstreetmap/josm/gui/mappaint/Environment.java
--- a/src/org/openstreetmap/josm/gui/mappaint/Environment.java	(revision 18732)
+++ b/src/org/openstreetmap/josm/gui/mappaint/Environment.java	(date 1684421822601)
@@ -12,6 +12,7 @@
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.WaySegment;
 import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.Context;
+import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
 import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.LinkSelector;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 
@@ -41,6 +42,9 @@
     public StyleSource source;
     private Context context = Context.PRIMITIVE;
 
+    /** The selector that is currently being evaluated */
+    private final Selector selector;
+
     /**
      * The name of the default layer. It is used if no layer is specified in the MapCSS rule
      */
@@ -97,6 +101,7 @@
      */
     public Environment() {
         // environment can be initialized later through with* methods
+        this.selector = null;
     }
 
     /**
@@ -106,7 +111,7 @@
      * @since 13810 (signature)
      */
     public Environment(IPrimitive osm) {
-        this.osm = osm;
+        this(osm, null, null, null);
     }
 
     /**
@@ -122,6 +127,7 @@
         this.mc = mc;
         this.layer = layer;
         this.source = source;
+        this.selector = null;
     }
 
     /**
@@ -131,6 +137,17 @@
      * @throws IllegalArgumentException if {@code param} is {@code null}
      */
     public Environment(Environment other) {
+        this(other, other.selector);
+    }
+
+    /**
+     * Creates a clone of the environment {@code other}.
+     *
+     * @param other the other environment. Must not be null.
+     * @param selector the selector for this environment. May be null.
+     * @throws IllegalArgumentException if {@code param} is {@code null}
+     */
+    private Environment(Environment other, Selector selector) {
         CheckParameterUtil.ensureParameterNotNull(other);
         this.osm = other.osm;
         this.mc = other.mc;
@@ -146,6 +163,7 @@
         this.crossingWaysMap = other.crossingWaysMap;
         this.mpAreaCache = other.mpAreaCache;
         this.toMatchForSurrounding = other.toMatchForSurrounding;
+        this.selector = selector;
     }
 
     /**
@@ -262,6 +280,16 @@
         return e;
     }
 
+    /**
+     * Creates a clone of this environment, with the selector set
+     * @param selector The selector to use
+     * @return A clone of this environment, with the specified selector
+     * @since xxx
+     */
+    public Environment withSelector(Selector selector) {
+        return new Environment(this, selector);
+    }
+
     /**
      * Determines if the context of this environment is {@link Context#LINK}.
      * @return {@code true} if the context of this environment is {@code Context#LINK}, {@code false} otherwise
@@ -303,6 +331,15 @@
         return null;
     }
 
+    /**
+     * Get the selector for this environment
+     * @return The selector. May be {@code null}.
+     * @since xxx
+     */
+    public Selector selector() {
+        return this.selector;
+    }
+
     /**
      * Clears all matching context information
      * @return this
Index: test/unit/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/test/unit/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerTest.java b/test/unit/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerTest.java
--- a/test/unit/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerTest.java	(revision 18732)
+++ b/test/unit/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerTest.java	(date 1684421839558)
@@ -95,7 +95,7 @@
         final MapCSSTagCheckerRule check = checks.get(0);
         assertNotNull(check);
         assertEquals("{0.key}=null is deprecated", check.getDescription(null));
-        assertEquals("fixRemove: {0.key}", check.fixCommands.get(0).toString());
+        assertEquals("fixRemove: <{0.key}>", check.fixCommands.get(0).toString());
         assertEquals("fixAdd: natural=wetland", check.fixCommands.get(1).toString());
         assertEquals("fixAdd: wetland=marsh", check.fixCommands.get(2).toString());
         final OsmPrimitive n1 = OsmUtils.createPrimitive("node natural=marsh");
