Repository: graphhopper/graphhopper
Pull request #2091 (2020-07-21 15:28:28)

Improve tag extraction
- [x] support for parsing short ton weight limits
- [x] move speed limit parsing into OSMValueExtractor
- [x] utility to check parsers using JSON data provided by https://taginfo.openstreetmap.org/

otbutz: We should probably add support for parsing non-numerical maxheight values: https://wiki.openstreetmap.org/wiki/Key:maxheight#Non-numerical_values

Currently we miss out on a lot of valuable information:

* default: 18545
* none: 12264
* below_default: 3045
* unsigned: 136
* no_indications: 84
otbutz: IMHO it's better to ignore `default` and its equivalents and handle non existing limits later using spatial rules as we're able to properly differentiate between countries.

But i've the extended the extractor to deal with maxheight `below_default` returning a rather conservative height limit of 2m which should be enough for regular cars to pass and while blocking HGV traffic.
karussell: Sorry, I completely missed this ... will merge now.

Patch:
diff --git a/core/src/main/java/com/graphhopper/routing/util/AbstractFlagEncoder.java b/core/src/main/java/com/graphhopper/routing/util/AbstractFlagEncoder.java
index aeb995dee..22df25242 100644
--- a/core/src/main/java/com/graphhopper/routing/util/AbstractFlagEncoder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/AbstractFlagEncoder.java
@@ -23,6 +23,7 @@ import com.graphhopper.reader.ReaderWay;
 import com.graphhopper.reader.osm.conditional.ConditionalOSMTagInspector;
 import com.graphhopper.reader.osm.conditional.DateRangeParser;
 import com.graphhopper.routing.ev.*;
+import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
 import com.graphhopper.storage.IntsRef;
 import com.graphhopper.util.*;
 import org.slf4j.Logger;
@@ -222,20 +223,27 @@ public abstract class AbstractFlagEncoder implements FlagEncoder {
     }
 
     /**
-     * @return -1 if no maxspeed found
+     * @return {@link Double#NaN} if no maxspeed found
      */
     protected double getMaxSpeed(ReaderWay way) {
-        double maxSpeed = parseSpeed(way.getTag("maxspeed"));
-        double fwdSpeed = parseSpeed(way.getTag("maxspeed:forward"));
-        if (fwdSpeed >= 0 && (maxSpeed < 0 || fwdSpeed < maxSpeed))
+        double maxSpeed = OSMValueExtractor.stringToKmh(way.getTag("maxspeed"));
+        double fwdSpeed = OSMValueExtractor.stringToKmh(way.getTag("maxspeed:forward"));
+        if (isValidSpeed(fwdSpeed) && (!isValidSpeed(maxSpeed) || fwdSpeed < maxSpeed))
             maxSpeed = fwdSpeed;
 
-        double backSpeed = parseSpeed(way.getTag("maxspeed:backward"));
-        if (backSpeed >= 0 && (maxSpeed < 0 || backSpeed < maxSpeed))
+        double backSpeed = OSMValueExtractor.stringToKmh(way.getTag("maxspeed:backward"));
+        if (isValidSpeed(backSpeed) && (!isValidSpeed(maxSpeed) || backSpeed < maxSpeed))
             maxSpeed = backSpeed;
 
         return maxSpeed;
     }
+    
+    /**
+     * @return <i>true</i> if the given speed is not {@link Double#NaN}
+     */
+    protected boolean isValidSpeed(double speed) {
+        return !Double.isNaN(speed);
+    }
 
     @Override
     public int hashCode() {
@@ -256,59 +264,6 @@ public abstract class AbstractFlagEncoder implements FlagEncoder {
         return toString().equals(afe.toString()) && encoderBit == afe.encoderBit && accessEnc.equals(afe.accessEnc);
     }
 
-    /**
-     * @return the speed in km/h
-     */
-    public static double parseSpeed(String str) {
-        if (Helper.isEmpty(str))
-            return -1;
-
-        // on some German autobahns and a very few other places
-        if ("none".equals(str))
-            return MaxSpeed.UNLIMITED_SIGN_SPEED;
-
-        if (str.endsWith(":rural") || str.endsWith(":trunk"))
-            return 80;
-
-        if (str.endsWith(":urban"))
-            return 50;
-
-        if (str.equals("walk") || str.endsWith(":living_street"))
-            return 6;
-
-        try {
-            int val;
-            // see https://en.wikipedia.org/wiki/Knot_%28unit%29#Definitions
-            int mpInteger = str.indexOf("mp");
-            if (mpInteger > 0) {
-                str = str.substring(0, mpInteger).trim();
-                val = Integer.parseInt(str);
-                return val * DistanceCalcEarth.KM_MILE;
-            }
-
-            int knotInteger = str.indexOf("knots");
-            if (knotInteger > 0) {
-                str = str.substring(0, knotInteger).trim();
-                val = Integer.parseInt(str);
-                return val * 1.852;
-            }
-
-            int kmInteger = str.indexOf("km");
-            if (kmInteger > 0) {
-                str = str.substring(0, kmInteger).trim();
-            } else {
-                kmInteger = str.indexOf("kph");
-                if (kmInteger > 0) {
-                    str = str.substring(0, kmInteger).trim();
-                }
-            }
-
-            return Integer.parseInt(str);
-        } catch (Exception ex) {
-            return -1;
-        }
-    }
-
     /**
      * Second parsing step. Invoked after splitting the edges. Currently used to offer a hook to
      * calculate precise speed values based on elevation data stored in the specified edge.
@@ -394,7 +349,7 @@ public abstract class AbstractFlagEncoder implements FlagEncoder {
      * @Deprecated
      */
     protected void setSpeed(boolean reverse, IntsRef edgeFlags, double speed) {
-        if (speed < 0 || Double.isNaN(speed))
+        if (!isValidSpeed(speed))
             throw new IllegalArgumentException("Speed cannot be negative or NaN: " + speed + ", flags:" + BitUtil.LITTLE.toBitString(edgeFlags));
 
         if (speed < speedFactor / 2) {
@@ -429,7 +384,7 @@ public abstract class AbstractFlagEncoder implements FlagEncoder {
     protected double applyMaxSpeed(ReaderWay way, double speed) {
         double maxSpeed = getMaxSpeed(way);
         // We obey speed limits
-        if (maxSpeed >= 0) {
+        if (isValidSpeed(maxSpeed)) {
             // We assume that the average speed is 90% of the allowed maximum
             return maxSpeed * 0.9;
         }
diff --git a/core/src/main/java/com/graphhopper/routing/util/BikeCommonFlagEncoder.java b/core/src/main/java/com/graphhopper/routing/util/BikeCommonFlagEncoder.java
index 1e34c84d9..31b9e1d8e 100644
--- a/core/src/main/java/com/graphhopper/routing/util/BikeCommonFlagEncoder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/BikeCommonFlagEncoder.java
@@ -289,12 +289,11 @@ abstract public class BikeCommonFlagEncoder extends AbstractFlagEncoder {
     @Override
     protected double applyMaxSpeed(ReaderWay way, double speed) {
         double maxSpeed = getMaxSpeed(way);
-        if (maxSpeed >= 0) {
-            // We strictly obey speed limits, see #600
-            if (speed > maxSpeed)
-                return maxSpeed;
+        // We strictly obey speed limits, see #600
+        if (isValidSpeed(maxSpeed) && speed > maxSpeed) {
+            return maxSpeed;
         }
-        if (speed > maxPossibleSpeed)
+        if (isValidSpeed(speed) && speed > maxPossibleSpeed)
             return maxPossibleSpeed;
         return speed;
     }
@@ -453,14 +452,14 @@ abstract public class BikeCommonFlagEncoder extends AbstractFlagEncoder {
         }
 
         double maxSpeed = getMaxSpeed(way);
-        if (preferHighwayTags.contains(highway) || maxSpeed > 0 && maxSpeed <= 30) {
-            if (maxSpeed < avoidSpeedLimit) {
+        if (preferHighwayTags.contains(highway) || (isValidSpeed(maxSpeed) && maxSpeed <= 30)) {
+            if (!isValidSpeed(maxSpeed) || maxSpeed < avoidSpeedLimit) {
                 weightToPrioMap.put(40d, PREFER.getValue());
                 if (way.hasTag("tunnel", intendedValues))
                     weightToPrioMap.put(40d, UNCHANGED.getValue());
             }
         } else if (avoidHighwayTags.contains(highway)
-                || maxSpeed >= avoidSpeedLimit && !"track".equals(highway)) {
+                || isValidSpeed(maxSpeed) && maxSpeed >= avoidSpeedLimit && !"track".equals(highway)) {
             weightToPrioMap.put(50d, REACH_DEST.getValue());
             if (way.hasTag("tunnel", intendedValues))
                 weightToPrioMap.put(50d, AVOID_AT_ALL_COSTS.getValue());
diff --git a/core/src/main/java/com/graphhopper/routing/util/CarFlagEncoder.java b/core/src/main/java/com/graphhopper/routing/util/CarFlagEncoder.java
index c102dc781..53075a2d4 100644
--- a/core/src/main/java/com/graphhopper/routing/util/CarFlagEncoder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/CarFlagEncoder.java
@@ -305,7 +305,7 @@ public class CarFlagEncoder extends AbstractFlagEncoder {
      */
     protected double applyBadSurfaceSpeed(ReaderWay way, double speed) {
         // limit speed if bad surface
-        if (badSurfaceSpeed > 0 && speed > badSurfaceSpeed && way.hasTag("surface", badSurfaceSpeedMap))
+        if (badSurfaceSpeed > 0 && isValidSpeed(speed) && speed > badSurfaceSpeed && way.hasTag("surface", badSurfaceSpeedMap))
             speed = badSurfaceSpeed;
         return speed;
     }
diff --git a/core/src/main/java/com/graphhopper/routing/util/FootFlagEncoder.java b/core/src/main/java/com/graphhopper/routing/util/FootFlagEncoder.java
index 4a52be071..aaf0299ea 100644
--- a/core/src/main/java/com/graphhopper/routing/util/FootFlagEncoder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/FootFlagEncoder.java
@@ -279,7 +279,7 @@ public class FootFlagEncoder extends AbstractFlagEncoder {
             weightToPrioMap.put(100d, PREFER.getValue());
 
         double maxSpeed = getMaxSpeed(way);
-        if (safeHighwayTags.contains(highway) || maxSpeed > 0 && maxSpeed <= 20) {
+        if (safeHighwayTags.contains(highway) || (isValidSpeed(maxSpeed) && maxSpeed <= 20)) {
             weightToPrioMap.put(40d, PREFER.getValue());
             if (way.hasTag("tunnel", intendedValues)) {
                 if (way.hasTag("sidewalk", sidewalksNoValues))
@@ -287,7 +287,7 @@ public class FootFlagEncoder extends AbstractFlagEncoder {
                 else
                     weightToPrioMap.put(40d, UNCHANGED.getValue());
             }
-        } else if (maxSpeed > 50 || avoidHighwayTags.contains(highway)) {
+        } else if ((isValidSpeed(maxSpeed) && maxSpeed > 50) || avoidHighwayTags.contains(highway)) {
             if (!way.hasTag("sidewalk", sidewalkValues))
                 weightToPrioMap.put(45d, AVOID_IF_POSSIBLE.getValue());
         }
diff --git a/core/src/main/java/com/graphhopper/routing/util/HikeFlagEncoder.java b/core/src/main/java/com/graphhopper/routing/util/HikeFlagEncoder.java
index 41b7a5e19..345ec8aab 100644
--- a/core/src/main/java/com/graphhopper/routing/util/HikeFlagEncoder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/HikeFlagEncoder.java
@@ -73,7 +73,7 @@ public class HikeFlagEncoder extends FootFlagEncoder {
             weightToPrioMap.put(100d, PREFER.getValue());
 
         double maxSpeed = getMaxSpeed(way);
-        if (safeHighwayTags.contains(highway) || maxSpeed > 0 && maxSpeed <= 20) {
+        if (safeHighwayTags.contains(highway) || (isValidSpeed(maxSpeed) && maxSpeed <= 20)) {
             weightToPrioMap.put(40d, PREFER.getValue());
             if (way.hasTag("tunnel", intendedValues)) {
                 if (way.hasTag("sidewalk", sidewalksNoValues))
@@ -81,7 +81,7 @@ public class HikeFlagEncoder extends FootFlagEncoder {
                 else
                     weightToPrioMap.put(40d, UNCHANGED.getValue());
             }
-        } else if (maxSpeed > 50 || avoidHighwayTags.contains(highway)) {
+        } else if ((isValidSpeed(maxSpeed) && maxSpeed > 50) || avoidHighwayTags.contains(highway)) {
             if (way.hasTag("sidewalk", sidewalksNoValues))
                 weightToPrioMap.put(45d, WORST.getValue());
             else
diff --git a/core/src/main/java/com/graphhopper/routing/util/MotorcycleFlagEncoder.java b/core/src/main/java/com/graphhopper/routing/util/MotorcycleFlagEncoder.java
index 29728bd9f..5ba2e7387 100644
--- a/core/src/main/java/com/graphhopper/routing/util/MotorcycleFlagEncoder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/MotorcycleFlagEncoder.java
@@ -20,7 +20,9 @@ package com.graphhopper.routing.util;
 import com.graphhopper.reader.ReaderWay;
 import com.graphhopper.routing.ev.DecimalEncodedValue;
 import com.graphhopper.routing.ev.EncodedValue;
+import com.graphhopper.routing.ev.MaxSpeed;
 import com.graphhopper.routing.ev.UnsignedDecimalEncodedValue;
+import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
 import com.graphhopper.routing.weighting.CurvatureWeighting;
 import com.graphhopper.routing.weighting.PriorityWeighting;
 import com.graphhopper.storage.IntsRef;
@@ -177,12 +179,12 @@ public class MotorcycleFlagEncoder extends CarFlagEncoder {
             double speed = getSpeed(way);
             speed = applyMaxSpeed(way, speed);
 
-            double maxMCSpeed = parseSpeed(way.getTag("maxspeed:motorcycle"));
-            if (maxMCSpeed > 0 && maxMCSpeed < speed)
+            double maxMCSpeed = OSMValueExtractor.stringToKmh(way.getTag("maxspeed:motorcycle"));
+            if (isValidSpeed(maxMCSpeed) && maxMCSpeed < speed)
                 speed = maxMCSpeed * 0.9;
 
             // limit speed to max 30 km/h if bad surface
-            if (speed > 30 && way.hasTag("surface", badSurfaceSpeedMap))
+            if (isValidSpeed(speed) && speed > 30 && way.hasTag("surface", badSurfaceSpeedMap))
                 speed = 30;
 
             boolean isRoundabout = roundaboutEnc.getBool(false, edgeFlags);
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxSpeedParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxSpeedParser.java
index ce2bc9ce7..2679f637c 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxSpeedParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxSpeedParser.java
@@ -23,7 +23,7 @@ import com.graphhopper.routing.ev.EncodedValue;
 import com.graphhopper.routing.ev.EncodedValueLookup;
 import com.graphhopper.routing.ev.MaxSpeed;
 import com.graphhopper.routing.ev.RoadClass;
-import com.graphhopper.routing.util.AbstractFlagEncoder;
+import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
 import com.graphhopper.routing.util.spatialrules.SpatialRuleSet;
 import com.graphhopper.routing.util.spatialrules.TransportationMode;
 import com.graphhopper.storage.IntsRef;
@@ -54,7 +54,7 @@ public class OSMMaxSpeedParser implements TagParser {
 
     @Override
     public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay way, boolean ferry, IntsRef relationFlags) {
-        double maxSpeed = AbstractFlagEncoder.parseSpeed(way.getTag("maxspeed"));
+        double maxSpeed = OSMValueExtractor.stringToKmh(way.getTag("maxspeed"));
 
         SpatialRuleSet spatialRuleSet = way.getTag("spatial_rule_set", null);
         if (spatialRuleSet != null && spatialRuleSet != SpatialRuleSet.EMPTY) {
@@ -62,26 +62,33 @@ public class OSMMaxSpeedParser implements TagParser {
             maxSpeed = spatialRuleSet.getMaxSpeed(roadClass, TransportationMode.MOTOR_VEHICLE, maxSpeed);
         }
 
-        double fwdSpeed = AbstractFlagEncoder.parseSpeed(way.getTag("maxspeed:forward"));
-        if (fwdSpeed < 0 && maxSpeed > 0)
+        double fwdSpeed = OSMValueExtractor.stringToKmh(way.getTag("maxspeed:forward"));
+        if (!isValidSpeed(fwdSpeed) && isValidSpeed(maxSpeed))
             fwdSpeed = maxSpeed;
         double maxPossibleSpeed = MaxSpeed.UNLIMITED_SIGN_SPEED;
-        if (fwdSpeed > maxPossibleSpeed)
+        if (isValidSpeed(fwdSpeed) && fwdSpeed > maxPossibleSpeed)
             fwdSpeed = maxPossibleSpeed;
 
-        double bwdSpeed = AbstractFlagEncoder.parseSpeed(way.getTag("maxspeed:backward"));
-        if (bwdSpeed < 0 && maxSpeed > 0)
+        double bwdSpeed = OSMValueExtractor.stringToKmh(way.getTag("maxspeed:backward"));
+        if (!isValidSpeed(bwdSpeed) && isValidSpeed(maxSpeed))
             bwdSpeed = maxSpeed;
-        if (bwdSpeed > maxPossibleSpeed)
+        if (isValidSpeed(bwdSpeed) && bwdSpeed > maxPossibleSpeed)
             bwdSpeed = maxPossibleSpeed;
 
-        if (fwdSpeed <= 0)
+        if (!isValidSpeed(fwdSpeed))
             fwdSpeed = UNSET_SPEED;
         carMaxSpeedEnc.setDecimal(false, edgeFlags, fwdSpeed);
 
-        if (bwdSpeed <= 0)
+        if (!isValidSpeed(bwdSpeed))
             bwdSpeed = UNSET_SPEED;
         carMaxSpeedEnc.setDecimal(true, edgeFlags, bwdSpeed);
         return edgeFlags;
     }
+    
+    /**
+     * @return <i>true</i> if the given speed is not {@link Double#NaN}
+     */
+    private boolean isValidSpeed(double speed) {
+        return !Double.isNaN(speed);
+    }
 }
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/helpers/OSMValueExtractor.java b/core/src/main/java/com/graphhopper/routing/util/parsers/helpers/OSMValueExtractor.java
index 90ebc9f75..e6951676c 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/helpers/OSMValueExtractor.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/helpers/OSMValueExtractor.java
@@ -7,7 +7,10 @@ import java.util.regex.Pattern;
 
 import com.graphhopper.reader.ReaderWay;
 import com.graphhopper.routing.ev.DecimalEncodedValue;
+import com.graphhopper.routing.ev.MaxSpeed;
 import com.graphhopper.storage.IntsRef;
+import com.graphhopper.util.DistanceCalcEarth;
+import com.graphhopper.util.Helper;
 
 public class OSMValueExtractor {
     
@@ -43,7 +46,10 @@ public class OSMValueExtractor {
             return Double.NaN;
 
         double factor = 1;
-        if (value.endsWith("t")) {
+        if (value.endsWith("st")) {
+            value = value.substring(0, value.length() - 2);
+            factor = 0.907194048807;
+        } else if (value.endsWith("t")) {
             value = value.substring(0, value.length() - 1);
         } else if (value.endsWith("lbs")) {
             value = value.substring(0, value.length() - 3);
@@ -136,4 +142,59 @@ public class OSMValueExtractor {
                 // only support '.' and no German decimals
                 || value.contains(",");
     }
+
+    /**
+     * @return the speed in km/h
+     */
+    public static double stringToKmh(String str) {
+        if (Helper.isEmpty(str))
+            return Double.NaN;
+    
+        // on some German autobahns and a very few other places
+        if ("none".equals(str))
+            return MaxSpeed.UNLIMITED_SIGN_SPEED;
+    
+        if (str.endsWith(":rural") || str.endsWith(":trunk"))
+            return 80;
+    
+        if (str.endsWith(":urban"))
+            return 50;
+    
+        if (str.equals("walk") || str.endsWith(":living_street"))
+            return 6;
+    
+        int mpInteger = str.indexOf("mp");
+        int knotInteger = str.indexOf("knots");
+        int kmInteger = str.indexOf("km");
+        int kphInteger = str.indexOf("kph");
+
+        double factor;
+        if (mpInteger > 0) {
+            str = str.substring(0, mpInteger).trim();
+            factor = DistanceCalcEarth.KM_MILE;
+        } else if (knotInteger > 0) {
+            str = str.substring(0, knotInteger).trim();
+            factor = 1.852; // see https://en.wikipedia.org/wiki/Knot_%28unit%29#Definitions
+        } else {
+            if (kmInteger > 0) {
+                str = str.substring(0, kmInteger).trim();
+            } else if (kphInteger > 0) {
+                str = str.substring(0, kphInteger).trim();
+            }
+            factor = 1;
+        }
+            
+        double value;
+        try {
+            value = Integer.parseInt(str) * factor;
+        } catch (Exception ex) {
+            return Double.NaN;
+        }
+        
+        if (value <= 0) {
+            return Double.NaN;
+        }
+        
+        return value;
+    }
 }
\ No newline at end of file
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRule.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRule.java
index 1700d072b..568369591 100644
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRule.java
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRule.java
@@ -39,7 +39,7 @@ public interface SpatialRule {
      *
      * @param roadClass       The highway type, e.g. {@link RoadClass#MOTORWAY}
      * @param transport       The mode of transportation
-     * @param currentMaxSpeed The current max speed value or -1 if no value has been set yet
+     * @param currentMaxSpeed The current max speed value or {@link Double#NaN} if no value has been set yet
      * @return the maximum speed value to be used
      */
     double getMaxSpeed(RoadClass roadClass, TransportationMode transport, double currentMaxSpeed);
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleSet.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleSet.java
index c2a289eb4..e86b25a88 100644
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleSet.java
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleSet.java
@@ -48,7 +48,7 @@ public class SpatialRuleSet {
      *
      * @param roadClass       The highway type, e.g. {@link RoadClass#MOTORWAY}
      * @param transport       The mode of transportation
-     * @param currentMaxSpeed The current max speed value or -1 if no value has been set yet
+     * @param currentMaxSpeed The current max speed value or {@link Double#NaN} if no value has been set yet
      * @return the maximum speed value to be used
      */
     public double getMaxSpeed(RoadClass roadClass, TransportationMode transport, double currentMaxSpeed) {
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/countries/AustriaSpatialRule.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/countries/AustriaSpatialRule.java
index 39773ef14..faead9686 100644
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/countries/AustriaSpatialRule.java
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/countries/AustriaSpatialRule.java
@@ -40,7 +40,7 @@ public class AustriaSpatialRule extends AbstractSpatialRule {
     
     @Override
     public double getMaxSpeed(RoadClass roadClass, TransportationMode transport, double currentMaxSpeed) {
-        if (currentMaxSpeed > 0 || transport != TransportationMode.MOTOR_VEHICLE) {
+        if (!Double.isNaN(currentMaxSpeed) || transport != TransportationMode.MOTOR_VEHICLE) {
             return currentMaxSpeed;
         }
         
@@ -62,7 +62,7 @@ public class AustriaSpatialRule extends AbstractSpatialRule {
         case LIVING_STREET:
             return 20;
         default:
-            return -1;
+            return Double.NaN;
         }
     }
     
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/countries/GermanySpatialRule.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/countries/GermanySpatialRule.java
index ad9983e2e..e1fb93fe8 100644
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/countries/GermanySpatialRule.java
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/countries/GermanySpatialRule.java
@@ -47,7 +47,7 @@ public class GermanySpatialRule extends AbstractSpatialRule {
      */
     @Override
     public double getMaxSpeed(RoadClass roadClass, TransportationMode transport, double currentMaxSpeed) {
-        if (currentMaxSpeed > 0 || transport != TransportationMode.MOTOR_VEHICLE) {
+        if (!Double.isNaN(currentMaxSpeed) || transport != TransportationMode.MOTOR_VEHICLE) {
             return currentMaxSpeed;
         }
         
@@ -69,7 +69,7 @@ public class GermanySpatialRule extends AbstractSpatialRule {
             case LIVING_STREET:
                 return 4;
             default:
-                return -1;
+                return Double.NaN;
         }
     }
     
diff --git a/tools/src/main/java/com/graphhopper/tools/TagInfoUtil.java b/tools/src/main/java/com/graphhopper/tools/TagInfoUtil.java
new file mode 100644
index 000000000..036572a28
--- /dev/null
+++ b/tools/src/main/java/com/graphhopper/tools/TagInfoUtil.java
@@ -0,0 +1,129 @@
+package com.graphhopper.tools;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
+
+public class TagInfoUtil {
+    
+    private static final String URL_TEMPLATE = "https://taginfo.openstreetmap.org/api/4/key/values?"
+                    + "filter=all&sortname=count&sortorder=desc&qtype=value&format=json&key=";
+    private static final Extractor TONS_EXTRACTOR  = OSMValueExtractor::stringToTons;
+    private static final Extractor METER_EXTRACTOR = OSMValueExtractor::stringToMeter;
+    private static final Extractor KMH_EXTRACTOR = OSMValueExtractor::stringToKmh;
+
+    public static void main(String[] args) throws IOException {
+        Map<String, Extractor> keyMap = new LinkedHashMap<>();
+        keyMap.put("maxweight",   TONS_EXTRACTOR);
+        keyMap.put("maxaxleload", TONS_EXTRACTOR);
+        keyMap.put("maxwidth",  METER_EXTRACTOR);
+        keyMap.put("maxheight", METER_EXTRACTOR);
+        keyMap.put("maxlength", METER_EXTRACTOR);
+        keyMap.put("maxspeed",  KMH_EXTRACTOR);
+        
+        for (Entry<String, Extractor> entry: keyMap.entrySet()) {
+            String key = entry.getKey();
+            Extractor extractor = entry.getValue();
+            analyzeTags(key, extractor);
+        }
+    }
+    
+    private static void analyzeTags(String key, Extractor extractor) throws IOException {
+        System.out.println("Tag: " + key);
+        Map<String, Tag> parsedMap = new LinkedHashMap<>();
+        List<Tag> failed = new ArrayList<>();
+        int count = 0;
+        for (Tag tag : loadTags(key)) {
+            count += tag.getCount();
+            double val = extractor.extract(tag.getValue());
+            if (Double.isNaN(val)) {
+                failed.add(tag);
+                continue;
+            }
+            
+            System.out.println("\"" + tag.getValue() + "\" -> " + val);
+            
+            String normalized = Double.toString(val);
+            if (parsedMap.containsKey(normalized)) {
+                Tag existing = parsedMap.get(normalized);
+                existing.setCount(existing.getCount() + tag.getCount());
+            } else {
+                parsedMap.put(normalized, new Tag(normalized, tag.getCount()));
+            }
+        }
+
+        for (Tag tag : failed) {
+            System.out.println("Unable to parse \"" + tag.getValue() + "\" (" + tag.getCount() + " occurrences)");
+        }
+        
+        int parsedCount = parsedMap.values().stream().mapToInt(Tag::getCount).sum();
+        double percentage = parsedCount / (double) count * 100;
+        System.out.println("Success rate: " + percentage + "%");
+    }
+
+    private static List<Tag> loadTags(String key) throws IOException {
+        JsonNode node;
+        try (InputStream in = new URL(URL_TEMPLATE + key).openStream(); BufferedInputStream bufferedIn = new BufferedInputStream(in)) {
+            node = new ObjectMapper().readTree(bufferedIn);
+        }
+        
+        List<Tag> tags = new ArrayList<>();
+        Iterator<JsonNode> iter = node.path("data").elements();
+        while (iter.hasNext()) {
+            JsonNode tagElement = iter.next();
+            String value = tagElement.path("value").asText();
+            int count = tagElement.path("count").asInt();
+            tags.add(new Tag(value, count));
+        }
+        
+        return tags;
+    }
+    
+    private interface Extractor {
+        double extract(String value);
+    }
+    
+    private static class Tag {
+        private final String value;
+        private int count;
+        
+        public Tag(String value, int count) {
+            this.value = value;
+            this.count = count;
+        }
+
+        public String getValue() {
+            return value;
+        }
+
+        public int getCount() {
+            return count;
+        }
+
+        public void setCount(int count) {
+            this.count = count;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("Tag [value=");
+            builder.append(value);
+            builder.append(", count=");
+            builder.append(count);
+            builder.append("]");
+            return builder.toString();
+        }        
+    }
+}


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/util/AbstractFlagEncoderTest.java b/core/src/test/java/com/graphhopper/routing/util/AbstractFlagEncoderTest.java
deleted file mode 100644
index e487dd235..000000000
--- a/core/src/test/java/com/graphhopper/routing/util/AbstractFlagEncoderTest.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- *  Licensed to GraphHopper GmbH under one or more contributor
- *  license agreements. See the NOTICE file distributed with this work for
- *  additional information regarding copyright ownership.
- *
- *  GraphHopper GmbH licenses this file to you under the Apache License,
- *  Version 2.0 (the "License"); you may not use this file except in
- *  compliance with the License. You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package com.graphhopper.routing.util;
-
-import org.junit.jupiter.api.Test;
-
-import static com.graphhopper.routing.util.AbstractFlagEncoder.parseSpeed;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author Peter Karich
- */
-public class AbstractFlagEncoderTest {
-    @Test
-    public void testParseSpeed() {
-        assertEquals(40, parseSpeed("40 km/h"), 1e-3);
-        assertEquals(40, parseSpeed("40km/h"), 1e-3);
-        assertEquals(40, parseSpeed("40kmh"), 1e-3);
-        assertEquals(64.374, parseSpeed("40mph"), 1e-3);
-        assertEquals(48.28, parseSpeed("30 mph"), 1e-3);
-        assertEquals(-1, parseSpeed(null), 1e-3);
-        assertEquals(18.52, parseSpeed("10 knots"), 1e-3);
-        assertEquals(19, parseSpeed("19 kph"), 1e-3);
-        assertEquals(19, parseSpeed("19kph"), 1e-3);
-
-        assertEquals(50, parseSpeed("RO:urban"), 1e-3);
-
-        assertEquals(80, parseSpeed("RU:rural"), 1e-3);
-
-        assertEquals(6, parseSpeed("walk"), 1e-3);
-    }
-}
diff --git a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMValueExtractorTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMValueExtractorTest.java
index ab1b012ce..feb5ff983 100644
--- a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMValueExtractorTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMValueExtractorTest.java
@@ -1,5 +1,6 @@
 package com.graphhopper.routing.util.parsers;
 
+import com.graphhopper.routing.ev.MaxSpeed;
 import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
 import org.junit.Test;
 
@@ -8,7 +9,7 @@ import static org.junit.Assert.assertTrue;
 
 public class OSMValueExtractorTest {
 
-    private final double DELTA = 0.01;
+    private final double DELTA = 0.001;
 
     @Test
     public void stringToTons() {
@@ -21,6 +22,7 @@ public class OSMValueExtractorTest {
         assertEquals(3, OSMValueExtractor.stringToTons("3 T"), DELTA);
         assertEquals(3, OSMValueExtractor.stringToTons("3ton"), DELTA);
         assertEquals(10, OSMValueExtractor.stringToTons("10000 kg"), DELTA);
+        assertEquals(25.401, OSMValueExtractor.stringToTons("28 st"), DELTA);
 
         // maximum gross weight
         assertEquals(6, OSMValueExtractor.stringToTons("6t mgw"), DELTA);
@@ -43,8 +45,8 @@ public class OSMValueExtractorTest {
         assertEquals(1.5, OSMValueExtractor.stringToMeter("1.5 m"), DELTA);
         assertEquals(1.5, OSMValueExtractor.stringToMeter("1.5   m"), DELTA);
         assertEquals(1.5, OSMValueExtractor.stringToMeter("1.5 meter"), DELTA);
-        assertEquals(1.5, OSMValueExtractor.stringToMeter("4 ft 11 in"), DELTA);
-        assertEquals(1.5, OSMValueExtractor.stringToMeter("4'11''"), DELTA);
+        assertEquals(1.499, OSMValueExtractor.stringToMeter("4 ft 11 in"), DELTA);
+        assertEquals(1.499, OSMValueExtractor.stringToMeter("4'11''"), DELTA);
 
 
         assertEquals(3, OSMValueExtractor.stringToMeter("3 m."), DELTA);
@@ -78,4 +80,30 @@ public class OSMValueExtractorTest {
     public void stringToMeterNaN3() {
         assertTrue(Double.isNaN(OSMValueExtractor.stringToMeter("default")));
     }
+
+    @Test
+    public void stringToKmh() {
+        assertEquals(40, OSMValueExtractor.stringToKmh("40 km/h"), DELTA);
+        assertEquals(40, OSMValueExtractor.stringToKmh("40km/h"), DELTA);
+        assertEquals(40, OSMValueExtractor.stringToKmh("40kmh"), DELTA);
+        assertEquals(64.374, OSMValueExtractor.stringToKmh("40mph"), DELTA);
+        assertEquals(48.28, OSMValueExtractor.stringToKmh("30 mph"), DELTA);
+        assertEquals(18.52, OSMValueExtractor.stringToKmh("10 knots"), DELTA);
+        assertEquals(19, OSMValueExtractor.stringToKmh("19 kph"), DELTA);
+        assertEquals(19, OSMValueExtractor.stringToKmh("19kph"), DELTA);
+
+        assertEquals(50, OSMValueExtractor.stringToKmh("RO:urban"), DELTA);
+
+        assertEquals(80, OSMValueExtractor.stringToKmh("RU:rural"), DELTA);
+
+        assertEquals(6, OSMValueExtractor.stringToKmh("walk"), DELTA);
+        assertEquals(MaxSpeed.UNLIMITED_SIGN_SPEED, OSMValueExtractor.stringToKmh("none"), DELTA);
+    }
+
+    @Test
+    public void stringToKmhNaN() {
+        assertTrue(Double.isNaN(OSMValueExtractor.stringToKmh(null)));
+        assertTrue(Double.isNaN(OSMValueExtractor.stringToKmh("0")));
+        assertTrue(Double.isNaN(OSMValueExtractor.stringToKmh("-20")));
+    }
 }
\ No newline at end of file