Repository: graphhopper/graphhopper (language: java)

Issue:
maxspeed=0.5 on a way triggers exception when using bicycle profile
**Describe the bug**
I found a spot where OSM contains ways tagged with `maxspeed=0.5`. This bad data triggers the following exception when using one of the bicycle profiles

```
java.lang.IllegalArgumentException: Speed was 0.5 but cannot be lower than 1.0

	at com.graphhopper.routing.util.parsers.AbstractAverageSpeedParser.setSpeed(AbstractAverageSpeedParser.java:42)
	at com.graphhopper.routing.util.parsers.BikeCommonAverageSpeedParser.handleWayTags(BikeCommonAverageSpeedParser.java:176)
	at com.graphhopper.routing.util.parsers.AbstractAverageSpeedParser.handleWayTags(AbstractAverageSpeedParser.java:54)
	at com.graphhopper.routing.util.OSMParsers.handleWayTags(OSMParsers.java:101)
	at com.graphhopper.routing.util.parsers.AbstractBikeTagParserTester.getSpeedFromFlags(AbstractBikeTagParserTester.java:103)
	at com.graphhopper.routing.util.parsers.BikeTagParserTest.testSmoothness(BikeTagParserTest.java:366)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

```
**To Reproduce**
Tag a way with tag  `maxspeed=0.5` and use a bicycle profile

**Expected behavior**
No exception

**System Information**

Latest master


Discussion / hints:
I'm not sure if this change is the way to go:

```
    protected void setSpeed(boolean reverse, int edgeId, EdgeIntAccess edgeIntAccess, double speed) {
        if (speed < avgSpeedEnc.getSmallestNonZeroValue() / 2) {
            avgSpeedEnc.setDecimal(reverse, edgeId, edgeIntAccess, avgSpeedEnc.getSmallestNonZeroValue());
        } else {
            avgSpeedEnc.setDecimal(reverse, edgeId, edgeIntAccess, speed);
        }
    }
```

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bf3a4ec08b..df8117b4acf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
 ### 11.0 [not yet released]
 
+- maxspeed<5 is ignored, maxspeed=none is ignored with some exceptions, maxspeed parsing and related constants were renamed #3077
+
 ### 10.0 [5 Nov 2024]
 
 - The config-example.yml uses a non-empty snap_preventions default array: [tunnel, bridge and ferry] for the /route endpoint
diff --git a/core/src/main/java/com/graphhopper/routing/ev/MaxSpeed.java b/core/src/main/java/com/graphhopper/routing/ev/MaxSpeed.java
index e8239955662..2943d4b3c84 100644
--- a/core/src/main/java/com/graphhopper/routing/ev/MaxSpeed.java
+++ b/core/src/main/java/com/graphhopper/routing/ev/MaxSpeed.java
@@ -18,7 +18,7 @@
 package com.graphhopper.routing.ev;
 
 /**
- * This EncodedValue stores maximum speed values for car in km/h. If not initialized it returns UNSET_SPEED.
+ * This EncodedValue stores maximum speed values for car in km/h.
  */
 public class MaxSpeed {
     public static final String KEY = "max_speed";
@@ -28,11 +28,11 @@ public class MaxSpeed {
      * not explicitly used in OSM and can be precisely returned for a factor of 5, 3, 2 and 1. It is fixed and
      * not DecimalEncodedValue.getMaxInt to allow special case handling.
      */
-    public static final double UNLIMITED_SIGN_SPEED = 150;
+    public static final double MAXSPEED_150 = 150;
     /**
      * The speed value used for road sections without known speed limit.
      */
-    public static final double UNSET_SPEED = Double.POSITIVE_INFINITY;
+    public static final double MAXSPEED_MISSING = Double.POSITIVE_INFINITY;
 
     public static DecimalEncodedValue create() {
         // if we would store only km/h we could live with a factor of 5 and only 5 bits
diff --git a/core/src/main/java/com/graphhopper/routing/util/MaxSpeedCalculator.java b/core/src/main/java/com/graphhopper/routing/util/MaxSpeedCalculator.java
index c4342807a03..46afd3efcda 100644
--- a/core/src/main/java/com/graphhopper/routing/util/MaxSpeedCalculator.java
+++ b/core/src/main/java/com/graphhopper/routing/util/MaxSpeedCalculator.java
@@ -4,8 +4,8 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.graphhopper.routing.ev.*;
 import com.graphhopper.routing.util.parsers.DefaultMaxSpeedParser;
+import com.graphhopper.routing.util.parsers.OSMMaxSpeedParser;
 import com.graphhopper.routing.util.parsers.TagParser;
-import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
 import com.graphhopper.storage.DataAccess;
 import com.graphhopper.storage.Directory;
 import com.graphhopper.storage.Graph;
@@ -77,8 +77,8 @@ private static void convertMaxspeed(Set<Map.Entry<String, List<RoadTypeImpl>>> e
 
                     if ("maxspeed".equals(tags.getKey())
                             || "maxspeed:advisory".equals(tags.getKey())) {
-                        double tmp = OSMValueExtractor.stringToKmh(tags.getValue());
-                        if (Double.isNaN(tmp))
+                        double tmp = OSMMaxSpeedParser.parseMaxspeedString(tags.getValue());
+                        if (tmp == MaxSpeed.MAXSPEED_MISSING || tmp == OSMMaxSpeedParser.MAXSPEED_NONE)
                             throw new IllegalStateException("illegal maxspeed " + tags.getValue());
                         newTags.put(tags.getKey(), "" + Math.round(tmp));
                     }
@@ -127,8 +127,8 @@ public void createDataAccessForParser(Directory directory) {
     }
 
     /**
-     * This method sets max_speed values where the value is UNSET_SPEED to a value determined by
-     * the default speed library which is country-dependent.
+     * This method sets max_speed values where the value is {@link MaxSpeed.MAXSPEED_MISSING} to a
+     * value determined by the default speed library which is country-dependent.
      */
     public void fillMaxSpeed(Graph graph, EncodingManager em) {
         // In DefaultMaxSpeedParser and in OSMMaxSpeedParser we don't have the rural/urban info,
@@ -148,21 +148,21 @@ public void fillMaxSpeed(Graph graph, EncodingManager em, Function<EdgeIteratorS
             double bwdMaxSpeedPureOSM = iter.getReverse(maxSpeedEnc);
 
             // skip speeds-library if max_speed is known for both directions
-            if (fwdMaxSpeedPureOSM != MaxSpeed.UNSET_SPEED
-                    && bwdMaxSpeedPureOSM != MaxSpeed.UNSET_SPEED) continue;
+            if (fwdMaxSpeedPureOSM != MaxSpeed.MAXSPEED_MISSING
+                    && bwdMaxSpeedPureOSM != MaxSpeed.MAXSPEED_MISSING) continue;
 
             double maxSpeed = isUrbanDensityFun.apply(iter)
                     ? urbanMaxSpeedEnc.getDecimal(false, iter.getEdge(), internalMaxSpeedStorage)
                     : ruralMaxSpeedEnc.getDecimal(false, iter.getEdge(), internalMaxSpeedStorage);
-            if (maxSpeed != MaxSpeed.UNSET_SPEED) {
+            if (maxSpeed != MaxSpeed.MAXSPEED_MISSING) {
                 if (maxSpeed == 0) {
                     // TODO fix properly: RestrictionSetter adds artificial edges for which
                     //  we didn't set the speed in DefaultMaxSpeedParser, #2914
-                    iter.set(maxSpeedEnc, MaxSpeed.UNSET_SPEED, MaxSpeed.UNSET_SPEED);
+                    iter.set(maxSpeedEnc, MaxSpeed.MAXSPEED_MISSING, MaxSpeed.MAXSPEED_MISSING);
                 } else {
                     iter.set(maxSpeedEnc,
-                            fwdMaxSpeedPureOSM == MaxSpeed.UNSET_SPEED ? maxSpeed : fwdMaxSpeedPureOSM,
-                            bwdMaxSpeedPureOSM == MaxSpeed.UNSET_SPEED ? maxSpeed : bwdMaxSpeedPureOSM);
+                            fwdMaxSpeedPureOSM == MaxSpeed.MAXSPEED_MISSING ? maxSpeed : fwdMaxSpeedPureOSM,
+                            bwdMaxSpeedPureOSM == MaxSpeed.MAXSPEED_MISSING ? maxSpeed : bwdMaxSpeedPureOSM);
                     iter.set(maxSpeedEstEnc, true);
                 }
             }
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/AbstractAverageSpeedParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/AbstractAverageSpeedParser.java
index 2749b4b19b7..f850a46fdd7 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/AbstractAverageSpeedParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/AbstractAverageSpeedParser.java
@@ -17,22 +17,6 @@ protected AbstractAverageSpeedParser(DecimalEncodedValue speedEnc, DecimalEncode
         this.ferrySpeedEnc = ferrySpeedEnc;
     }
 
-    /**
-     * @return {@link Double#NaN} if no maxspeed found
-     */
-    public static double getMaxSpeed(ReaderWay way, boolean bwd) {
-        double maxSpeed = OSMValueExtractor.stringToKmh(way.getTag("maxspeed"));
-        double directedMaxSpeed = OSMValueExtractor.stringToKmh(way.getTag(bwd ? "maxspeed:backward" : "maxspeed:forward"));
-        return isValidSpeed(directedMaxSpeed) ? directedMaxSpeed : maxSpeed;
-    }
-
-    /**
-     * @return <i>true</i> if the given speed is not {@link Double#NaN}
-     */
-    protected static boolean isValidSpeed(double speed) {
-        return !Double.isNaN(speed);
-    }
-
     public final DecimalEncodedValue getAverageSpeedEnc() {
         return avgSpeedEnc;
     }
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/BikeCommonAverageSpeedParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/BikeCommonAverageSpeedParser.java
index 712a2613134..0dd8570963c 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/BikeCommonAverageSpeedParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/BikeCommonAverageSpeedParser.java
@@ -118,9 +118,9 @@ protected BikeCommonAverageSpeedParser(DecimalEncodedValue speedEnc, EnumEncoded
      * @return The assumed average speed.
      */
     double applyMaxSpeed(ReaderWay way, double speed, boolean bwd) {
-        double maxSpeed = getMaxSpeed(way, bwd);
+        double maxSpeed = OSMMaxSpeedParser.parseMaxSpeed(way, bwd);
         // We strictly obey speed limits, see #600
-        return isValidSpeed(maxSpeed) && speed > maxSpeed ? maxSpeed : speed;
+        return Math.min(speed, maxSpeed);
     }
 
     @Override
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/BikeCommonPriorityParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/BikeCommonPriorityParser.java
index f703f3526d9..440e4e40271 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/BikeCommonPriorityParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/BikeCommonPriorityParser.java
@@ -1,10 +1,7 @@
 package com.graphhopper.routing.util.parsers;
 
 import com.graphhopper.reader.ReaderWay;
-import com.graphhopper.routing.ev.DecimalEncodedValue;
-import com.graphhopper.routing.ev.EdgeIntAccess;
-import com.graphhopper.routing.ev.EnumEncodedValue;
-import com.graphhopper.routing.ev.RouteNetwork;
+import com.graphhopper.routing.ev.*;
 import com.graphhopper.routing.util.FerrySpeedCalculator;
 import com.graphhopper.routing.util.PriorityCode;
 import com.graphhopper.storage.IntsRef;
@@ -16,8 +13,6 @@
 import static com.graphhopper.routing.ev.RouteNetwork.*;
 import static com.graphhopper.routing.util.PriorityCode.*;
 import static com.graphhopper.routing.util.parsers.AbstractAccessParser.INTENDED;
-import static com.graphhopper.routing.util.parsers.AbstractAverageSpeedParser.getMaxSpeed;
-import static com.graphhopper.routing.util.parsers.AbstractAverageSpeedParser.isValidSpeed;
 
 public abstract class BikeCommonPriorityParser implements TagParser {
 
@@ -169,15 +164,15 @@ void collect(ReaderWay way, double wayTypeSpeed, TreeMap<Double, PriorityCode> w
                 weightToPrioMap.put(100d, VERY_NICE);
         }
 
-        double maxSpeed = Math.max(getMaxSpeed(way, false), getMaxSpeed(way, true));
-        if (preferHighwayTags.contains(highway) || (isValidSpeed(maxSpeed) && maxSpeed <= 30)) {
-            if (!isValidSpeed(maxSpeed) || maxSpeed < avoidSpeedLimit) {
+        double maxSpeed = Math.max(OSMMaxSpeedParser.parseMaxSpeed(way, false), OSMMaxSpeedParser.parseMaxSpeed(way, true));
+        if (preferHighwayTags.contains(highway) || (maxSpeed != MaxSpeed.MAXSPEED_MISSING && maxSpeed <= 30)) {
+            if (maxSpeed == MaxSpeed.MAXSPEED_MISSING || maxSpeed < avoidSpeedLimit) {
                 weightToPrioMap.put(40d, PREFER);
                 if (way.hasTag("tunnel", INTENDED))
                     weightToPrioMap.put(40d, UNCHANGED);
             }
         } else if (avoidHighwayTags.containsKey(highway)
-                || isValidSpeed(maxSpeed) && maxSpeed >= avoidSpeedLimit && !"track".equals(highway)) {
+                || (maxSpeed != MaxSpeed.MAXSPEED_MISSING && maxSpeed >= avoidSpeedLimit && !"track".equals(highway))) {
             PriorityCode priorityCode = avoidHighwayTags.get(highway);
             weightToPrioMap.put(50d, priorityCode == null ? AVOID : priorityCode);
             if (way.hasTag("tunnel", INTENDED)) {
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/CarAverageSpeedParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/CarAverageSpeedParser.java
index 4ef43ed30ea..6b9ef246aec 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/CarAverageSpeedParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/CarAverageSpeedParser.java
@@ -143,8 +143,8 @@ public void handleWayTags(int edgeId, EdgeIntAccess edgeIntAccess, ReaderWay way
      * @return The assumed speed.
      */
     protected double applyMaxSpeed(ReaderWay way, double speed, boolean bwd) {
-        double maxSpeed = getMaxSpeed(way, bwd);
-        return Math.min(140, isValidSpeed(maxSpeed) ? Math.max(1, maxSpeed * 0.9) : speed);
+        double maxSpeed = OSMMaxSpeedParser.parseMaxSpeed(way, bwd);
+        return maxSpeed != MaxSpeed.MAXSPEED_MISSING ? Math.max(1, maxSpeed * 0.9) : speed;
     }
 
     /**
@@ -154,7 +154,7 @@ protected double applyMaxSpeed(ReaderWay way, double speed, boolean bwd) {
      */
     protected double applyBadSurfaceSpeed(ReaderWay way, double speed) {
         // limit speed if bad surface
-        if (badSurfaceSpeed > 0 && isValidSpeed(speed) && speed > badSurfaceSpeed) {
+        if (badSurfaceSpeed > 0 && speed > badSurfaceSpeed) {
             String surface = way.getTag("surface", "");
             int colonIndex = surface.indexOf(":");
             if (colonIndex != -1)
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/DefaultMaxSpeedParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/DefaultMaxSpeedParser.java
index a143b758cd8..da9bd64528f 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/DefaultMaxSpeedParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/DefaultMaxSpeedParser.java
@@ -1,10 +1,7 @@
 package com.graphhopper.routing.util.parsers;
 
 import com.graphhopper.reader.ReaderWay;
-import com.graphhopper.routing.ev.Country;
-import com.graphhopper.routing.ev.DecimalEncodedValue;
-import com.graphhopper.routing.ev.EdgeIntAccess;
-import com.graphhopper.routing.ev.State;
+import com.graphhopper.routing.ev.*;
 import com.graphhopper.storage.IntsRef;
 import de.westnordost.osm_legal_default_speeds.LegalDefaultSpeeds;
 
@@ -13,9 +10,8 @@
 import java.util.LinkedHashMap;
 import java.util.Map;
 
-import static com.graphhopper.routing.ev.MaxSpeed.UNLIMITED_SIGN_SPEED;
-import static com.graphhopper.routing.ev.MaxSpeed.UNSET_SPEED;
-import static com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor.stringToKmh;
+import static com.graphhopper.routing.ev.MaxSpeed.MAXSPEED_150;
+import static com.graphhopper.routing.ev.MaxSpeed.MAXSPEED_MISSING;
 
 public class DefaultMaxSpeedParser implements TagParser {
     private final LegalDefaultSpeeds speeds;
@@ -37,9 +33,9 @@ public void init(DecimalEncodedValue ruralMaxSpeedEnc, DecimalEncodedValue urban
     public void handleWayTags(int edgeId, EdgeIntAccess _ignoreAccess, ReaderWay way, IntsRef relationFlags) {
         if (externalAccess == null)
             throw new IllegalArgumentException("Call init before using " + getClass().getName());
-        double maxSpeed = stringToKmh(way.getTag("maxspeed"));
+        double maxSpeed = Math.max(OSMMaxSpeedParser.parseMaxSpeed(way, false), OSMMaxSpeedParser.parseMaxSpeed(way, true));
         Integer ruralSpeedInt = null, urbanSpeedInt = null;
-        if (Double.isNaN(maxSpeed)) {
+        if (maxSpeed == MAXSPEED_MISSING) {
             Country country = way.getTag("country", Country.MISSING);
             State state = way.getTag("country_state", State.MISSING);
             if (country != Country.MISSING) {
@@ -56,7 +52,7 @@ public void handleWayTags(int edgeId, EdgeIntAccess _ignoreAccess, ReaderWay way
                     if (tmpResult != null) {
                         internRes.rural = parseInt(tmpResult.getTags().get("maxspeed"));
                         if (internRes.rural == null && "130".equals(tmpResult.getTags().get("maxspeed:advisory")))
-                            internRes.rural = (int) UNLIMITED_SIGN_SPEED;
+                            internRes.rural = (int) MAXSPEED_150;
                     }
 
                     tmpResult = speeds.getSpeedLimits(code,
@@ -64,7 +60,7 @@ public void handleWayTags(int edgeId, EdgeIntAccess _ignoreAccess, ReaderWay way
                     if (tmpResult != null) {
                         internRes.urban = parseInt(tmpResult.getTags().get("maxspeed"));
                         if (internRes.urban == null && "130".equals(tmpResult.getTags().get("maxspeed:advisory")))
-                            internRes.urban = (int) UNLIMITED_SIGN_SPEED;
+                            internRes.urban = (int) MAXSPEED_150;
                     }
                     return internRes;
                 });
@@ -74,8 +70,8 @@ public void handleWayTags(int edgeId, EdgeIntAccess _ignoreAccess, ReaderWay way
             }
         }
 
-        urbanMaxSpeedEnc.setDecimal(false, edgeId, externalAccess, urbanSpeedInt == null ? UNSET_SPEED : urbanSpeedInt);
-        ruralMaxSpeedEnc.setDecimal(false, edgeId, externalAccess, ruralSpeedInt == null ? UNSET_SPEED : ruralSpeedInt);
+        urbanMaxSpeedEnc.setDecimal(false, edgeId, externalAccess, urbanSpeedInt == null ? MAXSPEED_MISSING : urbanSpeedInt);
+        ruralMaxSpeedEnc.setDecimal(false, edgeId, externalAccess, ruralSpeedInt == null ? MAXSPEED_MISSING : ruralSpeedInt);
     }
 
     private Map<String, String> filter(Map<String, Object> tags) {
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/FootPriorityParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/FootPriorityParser.java
index 7ace2a62f39..70da54a6990 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/FootPriorityParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/FootPriorityParser.java
@@ -11,8 +11,6 @@
 import static com.graphhopper.routing.ev.RouteNetwork.*;
 import static com.graphhopper.routing.util.PriorityCode.*;
 import static com.graphhopper.routing.util.parsers.AbstractAccessParser.INTENDED;
-import static com.graphhopper.routing.util.parsers.AbstractAverageSpeedParser.getMaxSpeed;
-import static com.graphhopper.routing.util.parsers.AbstractAverageSpeedParser.isValidSpeed;
 
 public class FootPriorityParser implements TagParser {
     final Set<String> safeHighwayTags = new HashSet<>();
@@ -108,8 +106,8 @@ void collect(ReaderWay way, TreeMap<Double, PriorityCode> weightToPrioMap) {
             weightToPrioMap.put(100d, VERY_BAD); // see #3035
         }
 
-        double maxSpeed = Math.max(getMaxSpeed(way, false), getMaxSpeed(way, true));
-        if (safeHighwayTags.contains(highway) || (isValidSpeed(maxSpeed) && maxSpeed <= 20)) {
+        double maxSpeed = Math.max(OSMMaxSpeedParser.parseMaxSpeed(way, false), OSMMaxSpeedParser.parseMaxSpeed(way, true));
+        if (safeHighwayTags.contains(highway) || (maxSpeed != MaxSpeed.MAXSPEED_MISSING && maxSpeed <= 20)) {
             weightToPrioMap.put(40d, PREFER);
             if (way.hasTag("tunnel", INTENDED)) {
                 if (way.hasTag("sidewalk", sidewalksNoValues))
@@ -117,7 +115,7 @@ void collect(ReaderWay way, TreeMap<Double, PriorityCode> weightToPrioMap) {
                 else
                     weightToPrioMap.put(40d, UNCHANGED);
             }
-        } else if ((isValidSpeed(maxSpeed) && maxSpeed > 50) || avoidHighwayTags.containsKey(highway)) {
+        } else if ((maxSpeed != MaxSpeed.MAXSPEED_MISSING && maxSpeed > 50) || avoidHighwayTags.containsKey(highway)) {
             PriorityCode priorityCode = avoidHighwayTags.get(highway);
             if (way.hasTag("sidewalk", sidewalksNoValues))
                 weightToPrioMap.put(40d, priorityCode == null ? BAD : priorityCode);
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 3b01bf826fc..d0ea906712c 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
@@ -21,13 +21,20 @@
 import com.graphhopper.routing.ev.DecimalEncodedValue;
 import com.graphhopper.routing.ev.EdgeIntAccess;
 import com.graphhopper.routing.ev.MaxSpeed;
-import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
 import com.graphhopper.storage.IntsRef;
+import com.graphhopper.util.DistanceCalcEarth;
+import com.graphhopper.util.Helper;
 
-import static com.graphhopper.routing.ev.MaxSpeed.UNSET_SPEED;
+import static com.graphhopper.routing.ev.MaxSpeed.MAXSPEED_150;
+import static com.graphhopper.routing.ev.MaxSpeed.MAXSPEED_MISSING;
 
 public class OSMMaxSpeedParser implements TagParser {
 
+    /**
+     * Special value to represent `maxspeed=none` internally, not exposed via the maxspeed encoded value
+     */
+    public static final double MAXSPEED_NONE = -1;
+
     private final DecimalEncodedValue carMaxSpeedEnc;
 
     public OSMMaxSpeedParser(DecimalEncodedValue carMaxSpeedEnc) {
@@ -39,24 +46,89 @@ public OSMMaxSpeedParser(DecimalEncodedValue carMaxSpeedEnc) {
 
     @Override
     public void handleWayTags(int edgeId, EdgeIntAccess edgeIntAccess, ReaderWay way, IntsRef relationFlags) {
-        carMaxSpeedEnc.setDecimal(false, edgeId, edgeIntAccess, getMaxSpeed(way, false));
-        carMaxSpeedEnc.setDecimal(true, edgeId, edgeIntAccess, getMaxSpeed(way, true));
+        carMaxSpeedEnc.setDecimal(false, edgeId, edgeIntAccess, parseMaxSpeed(way, false));
+        carMaxSpeedEnc.setDecimal(true, edgeId, edgeIntAccess, parseMaxSpeed(way, true));
+    }
+
+    /**
+     * @return The maxspeed for the given way. It can be anything between 0 and {@link MaxSpeed.MAXSPEED_150},
+     *         or {@link MaxSpeed.MAXSPEED_MISSING} in case there is no valid maxspeed tagged for this way in this direction.
+     */
+    public static double parseMaxSpeed(ReaderWay way, boolean reverse) {
+        double directedMaxSpeed = parseMaxSpeedTag(way, reverse ? "maxspeed:backward" : "maxspeed:forward");
+        if (directedMaxSpeed != MAXSPEED_MISSING)
+            return directedMaxSpeed;
+        else {
+            return parseMaxSpeedTag(way, "maxspeed");
+        }
     }
 
-    private double getMaxSpeed(ReaderWay way, boolean reverse) {
-        final double maxSpeed = OSMValueExtractor.stringToKmh(way.getTag("maxspeed"));
-        final double directedMaxSpeed = OSMValueExtractor.stringToKmh(way.getTag(reverse ? "maxspeed:backward" : "maxspeed:forward"));
-        return isValidSpeed(directedMaxSpeed)
-                ? Math.min(directedMaxSpeed, MaxSpeed.UNLIMITED_SIGN_SPEED)
-                : isValidSpeed(maxSpeed)
-                ? Math.min(maxSpeed, MaxSpeed.UNLIMITED_SIGN_SPEED)
-                : UNSET_SPEED;
+    private static double parseMaxSpeedTag(ReaderWay way, String tag) {
+        double maxSpeed = parseMaxspeedString(way.getTag(tag));
+        if (maxSpeed != MAXSPEED_MISSING && maxSpeed != MAXSPEED_NONE)
+            // there is no actual use for maxspeeds above 150 so we simply truncate here
+            return Math.min(MAXSPEED_150, maxSpeed);
+        else if (maxSpeed == MAXSPEED_NONE && way.hasTag("highway", "motorway", "motorway_link", "trunk", "trunk_link", "primary"))
+            // We ignore maxspeed=none with some exceptions where unlimited speed is actually allowed like on some
+            // motorways, trunks and (very rarely) primary roads in Germany, or the Isle of Man. In other cases
+            // maxspeed=none is only used because mappers have a false understanding of this tag.
+            return MaxSpeed.MAXSPEED_150;
+        else
+            return MAXSPEED_MISSING;
     }
 
     /**
-     * @return <i>true</i> if the given speed is not {@link Double#NaN}
+     ** @return the speed in km/h, or {@link MAXSPEED_MISSING} if the string is invalid, or {@link MAXSPEED_NONE} in case it equals 'none'
      */
-    private boolean isValidSpeed(double speed) {
-        return !Double.isNaN(speed);
+    public static double parseMaxspeedString(String str) {
+        if (Helper.isEmpty(str))
+            return MAXSPEED_MISSING;
+
+        if ("walk".equals(str.trim()))
+            return 6;
+
+        if ("none".equals(str.trim()))
+            // Special case intended to be used when there is actually no speed limit and drivers
+            // can go as fast as they want like on parts of the German Autobahn. However, in OSM
+            // this is sometimes misused by mappers trying to indicate that there is no additional
+            // sign apart from the general speed limit.
+            return MAXSPEED_NONE;
+
+        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 = Double.parseDouble(str) * factor;
+        } catch (Exception ex) {
+            return MAXSPEED_MISSING;
+        }
+
+        if (value < 4.8)
+            // We consider maxspeed < 4.8km/h a bug in OSM data and act as if the tag wasn't there.
+            // The limit is chosen such that maxspeed=3mph is still valid, because there actually are
+            // some road signs using 3mph.
+            // https://github.com/graphhopper/graphhopper/pull/3077#discussion_r1826842203
+            return MAXSPEED_MISSING;
+
+        return value;
     }
 }
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 dd509a1eaf3..82caff0034e 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
@@ -3,9 +3,6 @@
 import com.graphhopper.reader.ReaderWay;
 import com.graphhopper.routing.ev.DecimalEncodedValue;
 import com.graphhopper.routing.ev.EdgeIntAccess;
-import com.graphhopper.routing.ev.MaxSpeed;
-import com.graphhopper.util.DistanceCalcEarth;
-import com.graphhopper.util.Helper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -168,52 +165,4 @@ public static boolean isInvalidValue(String value) {
                 || value.contains(",");
     }
 
-    /**
-     * @return the speed in km/h
-     */
-    public static double stringToKmh(String str) {
-        if (Helper.isEmpty(str))
-            return Double.NaN;
-
-        if ("walk".equals(str))
-            return 6;
-
-        // on some German autobahns and a very few other places
-        if ("none".equals(str))
-            return MaxSpeed.UNLIMITED_SIGN_SPEED;
-
-        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 = Double.parseDouble(str) * factor;
-        } catch (Exception ex) {
-            return Double.NaN;
-        }
-
-        if (value <= 0) {
-            return Double.NaN;
-        }
-
-        return value;
-    }
 }
diff --git a/tools/src/main/java/com/graphhopper/tools/TagInfoUtil.java b/tools/src/main/java/com/graphhopper/tools/TagInfoUtil.java
index 036572a2813..8464835f22a 100644
--- a/tools/src/main/java/com/graphhopper/tools/TagInfoUtil.java
+++ b/tools/src/main/java/com/graphhopper/tools/TagInfoUtil.java
@@ -13,6 +13,7 @@
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.graphhopper.routing.util.parsers.OSMMaxSpeedParser;
 import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
 
 public class TagInfoUtil {
@@ -21,7 +22,7 @@ public class TagInfoUtil {
                     + "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;
+    private static final Extractor MAXSPEED_EXTRACTOR = OSMMaxSpeedParser::parseMaxspeedString;
 
     public static void main(String[] args) throws IOException {
         Map<String, Extractor> keyMap = new LinkedHashMap<>();
@@ -30,7 +31,7 @@ public static void main(String[] args) throws IOException {
         keyMap.put("maxwidth",  METER_EXTRACTOR);
         keyMap.put("maxheight", METER_EXTRACTOR);
         keyMap.put("maxlength", METER_EXTRACTOR);
-        keyMap.put("maxspeed",  KMH_EXTRACTOR);
+        keyMap.put("maxspeed", MAXSPEED_EXTRACTOR);
         
         for (Entry<String, Extractor> entry: keyMap.entrySet()) {
             String key = entry.getKey();


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/util/MaxSpeedCalculatorTest.java b/core/src/test/java/com/graphhopper/routing/util/MaxSpeedCalculatorTest.java
index d3e9661a5ec..797d28b3cae 100644
--- a/core/src/test/java/com/graphhopper/routing/util/MaxSpeedCalculatorTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/MaxSpeedCalculatorTest.java
@@ -14,8 +14,8 @@
 import java.util.HashMap;
 import java.util.Map;
 
-import static com.graphhopper.routing.ev.MaxSpeed.UNLIMITED_SIGN_SPEED;
-import static com.graphhopper.routing.ev.MaxSpeed.UNSET_SPEED;
+import static com.graphhopper.routing.ev.MaxSpeed.MAXSPEED_150;
+import static com.graphhopper.routing.ev.MaxSpeed.MAXSPEED_MISSING;
 import static com.graphhopper.routing.ev.UrbanDensity.CITY;
 import static com.graphhopper.routing.ev.UrbanDensity.RURAL;
 import static org.junit.jupiter.api.Assertions.*;
@@ -55,21 +55,21 @@ public void setup() {
     public void internalMaxSpeed() {
         EdgeIntAccess storage = calc.getInternalMaxSpeedStorage();
         DecimalEncodedValue ruralEnc = calc.getRuralMaxSpeedEnc();
-        ruralEnc.setDecimal(false, 0, storage, UNSET_SPEED);
-        assertEquals(UNSET_SPEED, ruralEnc.getDecimal(false, 0, storage));
+        ruralEnc.setDecimal(false, 0, storage, MAXSPEED_MISSING);
+        assertEquals(MAXSPEED_MISSING, ruralEnc.getDecimal(false, 0, storage));
 
         ruralEnc.setDecimal(false, 1, storage, 33);
         assertEquals(34, ruralEnc.getDecimal(false, 1, storage));
 
         DecimalEncodedValue urbanEnc = calc.getUrbanMaxSpeedEnc();
-        urbanEnc.setDecimal(false, 1, storage, UNSET_SPEED);
-        assertEquals(UNSET_SPEED, urbanEnc.getDecimal(false, 1, storage));
+        urbanEnc.setDecimal(false, 1, storage, MAXSPEED_MISSING);
+        assertEquals(MAXSPEED_MISSING, urbanEnc.getDecimal(false, 1, storage));
 
         urbanEnc.setDecimal(false, 0, storage, 46);
         assertEquals(46, urbanEnc.getDecimal(false, 0, storage));
 
         // check that they are not modified
-        assertEquals(UNSET_SPEED, ruralEnc.getDecimal(false, 0, storage));
+        assertEquals(MAXSPEED_MISSING, ruralEnc.getDecimal(false, 0, storage));
         assertEquals(34, ruralEnc.getDecimal(false, 1, storage));
     }
 
@@ -95,7 +95,7 @@ public void testCityGermany() {
         way.setTag("highway", "motorway");
         edge = createEdge(way).set(urbanDensity, CITY);
         calc.fillMaxSpeed(graph, em);
-        assertEquals(UNLIMITED_SIGN_SPEED, edge.get(maxSpeedEnc), 1);
+        assertEquals(MAXSPEED_150, edge.get(maxSpeedEnc), 1);
 
         way = new ReaderWay(0L);
         way.setTag("country", Country.DEU);
@@ -129,7 +129,7 @@ public void testRuralGermany() {
         way.setTag("highway", "motorway");
         edge = createEdge(way).set(urbanDensity, RURAL);
         calc.fillMaxSpeed(graph, em);
-        assertEquals(UNLIMITED_SIGN_SPEED, edge.get(maxSpeedEnc), 1);
+        assertEquals(MAXSPEED_150, edge.get(maxSpeedEnc), 1);
 
         way = new ReaderWay(0L);
         way.setTag("country", Country.DEU);
@@ -305,6 +305,6 @@ public void testUnsupportedCountry() {
         way.setTag("highway", "primary");
         EdgeIteratorState edge = createEdge(way).set(urbanDensity, CITY);
         calc.fillMaxSpeed(graph, em);
-        assertEquals(UNSET_SPEED, edge.get(maxSpeedEnc), 1);
+        assertEquals(MAXSPEED_MISSING, edge.get(maxSpeedEnc), 1);
     }
 }
diff --git a/core/src/test/java/com/graphhopper/routing/util/parsers/BikeTagParserTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/BikeTagParserTest.java
index 3ede4f38325..1ac2aeb8c89 100644
--- a/core/src/test/java/com/graphhopper/routing/util/parsers/BikeTagParserTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/BikeTagParserTest.java
@@ -363,6 +363,14 @@ public void testSmoothness() {
         assertEquals(MIN_SPEED, getSpeedFromFlags(way), 0.01);
     }
 
+    @Test
+    public void testLowMaxSpeedIsIgnored() {
+        ReaderWay way = new ReaderWay(1);
+        way.setTag("highway", "residential");
+        way.setTag("maxspeed", "3");
+        assertEquals(18, getSpeedFromFlags(way), 0.01);
+    }
+
     @Test
     public void testCycleway() {
         ReaderWay way = new ReaderWay(1);
diff --git a/core/src/test/java/com/graphhopper/routing/util/parsers/CarTagParserTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/CarTagParserTest.java
index 148b8f1caeb..728ab85c5b5 100644
--- a/core/src/test/java/com/graphhopper/routing/util/parsers/CarTagParserTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/CarTagParserTest.java
@@ -276,7 +276,7 @@ public void testMaxSpeed() {
         EdgeIntAccess edgeIntAccess = ArrayEdgeIntAccess.createFromBytes(em.getBytesForFlags());
         int edgeId = 0;
         speedParser.handleWayTags(edgeId, edgeIntAccess, way);
-        assertEquals(140, avSpeedEnc.getDecimal(false, edgeId, edgeIntAccess), 1e-1);
+        assertEquals(136, avSpeedEnc.getDecimal(false, edgeId, edgeIntAccess), 1e-1);
 
         way = new ReaderWay(1);
         way.setTag("highway", "primary");
@@ -607,19 +607,6 @@ public void testMaxValue() {
         assertEquals(101.5, smallFactorSpeedEnc.getDecimal(false, edgeId, edgeIntAccess), .1);
     }
 
-    @Test
-    public void testSetToMaxSpeed() {
-        ReaderWay way = new ReaderWay(12);
-        way.setTag("maxspeed", "90");
-        assertEquals(90, AbstractAverageSpeedParser.getMaxSpeed(way, false), 1e-2);
-
-        way = new ReaderWay(12);
-        way.setTag("maxspeed", "90");
-        way.setTag("maxspeed:backward", "50");
-        assertEquals(90, AbstractAverageSpeedParser.getMaxSpeed(way, false), 1e-2);
-        assertEquals(50, AbstractAverageSpeedParser.getMaxSpeed(way, true), 1e-2);
-    }
-
     @Test
     public void testCombination() {
         ReaderWay way = new ReaderWay(123);
diff --git a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMMaxSpeedParserTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMMaxSpeedParserTest.java
index 74abd7ace03..ec04ecde62a 100644
--- a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMMaxSpeedParserTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMMaxSpeedParserTest.java
@@ -21,6 +21,8 @@
 import com.graphhopper.routing.ev.*;
 import com.graphhopper.storage.IntsRef;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
@@ -51,4 +53,114 @@ void countryRule() {
         assertEquals(40, maxSpeedEnc.getDecimal(true, edgeId, edgeIntAccess), .1);
     }
 
+    @Test
+    public void parseMaxSpeed() {
+        ReaderWay way = new ReaderWay(12);
+        way.setTag("maxspeed", "90");
+        assertEquals(90, OSMMaxSpeedParser.parseMaxSpeed(way, false), 1e-2);
+
+        way = new ReaderWay(12);
+        way.setTag("maxspeed", "90");
+        way.setTag("maxspeed:backward", "50");
+        assertEquals(90, OSMMaxSpeedParser.parseMaxSpeed(way, false), 1e-2);
+        assertEquals(50, OSMMaxSpeedParser.parseMaxSpeed(way, true), 1e-2);
+
+        way = new ReaderWay(12);
+        way.setTag("maxspeed", "none");
+        assertEquals(MaxSpeed.MAXSPEED_MISSING, OSMMaxSpeedParser.parseMaxSpeed(way, false), 1e-2);
+
+        way = new ReaderWay(12);
+        way.setTag("maxspeed", "none");
+        way.setTag("highway", "secondary");
+        assertEquals(MaxSpeed.MAXSPEED_MISSING, OSMMaxSpeedParser.parseMaxSpeed(way, false), 1e-2);
+
+        way = new ReaderWay(12);
+        way.setTag("maxspeed", "none");
+        way.setTag("highway", "motorway");
+        assertEquals(MaxSpeed.MAXSPEED_150, OSMMaxSpeedParser.parseMaxSpeed(way, false), 1e-2);
+
+        way = new ReaderWay(12);
+        // we ignore low maxspeeds as they are mostly bugs, see discussion in #3077
+        way.setTag("maxspeed", "3");
+        assertEquals(MaxSpeed.MAXSPEED_MISSING, OSMMaxSpeedParser.parseMaxSpeed(way, false), 1e-2);
+
+        way = new ReaderWay(12);
+        // maxspeed=5 is rather popular
+        way.setTag("maxspeed", "5");
+        assertEquals(5, OSMMaxSpeedParser.parseMaxSpeed(way, false), 1e-2);
+
+        way = new ReaderWay(12);
+        // maxspeed=3mph is used for a few traffic signs, so this is the smallest we accept
+        way.setTag("maxspeed", "3mph");
+        assertEquals(4.83, OSMMaxSpeedParser.parseMaxSpeed(way, false), 1e-2);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "motorway",
+            "motorway_link",
+            "trunk",
+            "trunk_link",
+            "primary"
+    })
+    void maxSpeedNone(String highway) {
+        DecimalEncodedValue maxSpeedEnc = MaxSpeed.create();
+        maxSpeedEnc.init(new EncodedValue.InitializerConfig());
+        OSMMaxSpeedParser parser = new OSMMaxSpeedParser(maxSpeedEnc);
+        IntsRef relFlags = new IntsRef(2);
+        EdgeIntAccess edgeIntAccess = new ArrayEdgeIntAccess(1);
+        int edgeId = 0;
+        assertEquals(0, maxSpeedEnc.getDecimal(false, edgeId, edgeIntAccess), .1);
+        ReaderWay way = new ReaderWay(29L);
+        way.setTag("highway", highway);
+        way.setTag("maxspeed", "none");
+        parser.handleWayTags(edgeId, edgeIntAccess, way, relFlags);
+        assertEquals(MaxSpeed.MAXSPEED_150, maxSpeedEnc.getDecimal(false, edgeId, edgeIntAccess), .1);
+    }
+
+    @Test
+    void smallMaxSpeed() {
+        DecimalEncodedValue maxSpeedEnc = MaxSpeed.create();
+        maxSpeedEnc.init(new EncodedValue.InitializerConfig());
+        OSMMaxSpeedParser parser = new OSMMaxSpeedParser(maxSpeedEnc);
+        IntsRef relFlags = new IntsRef(2);
+        EdgeIntAccess edgeIntAccess = new ArrayEdgeIntAccess(1);
+        int edgeId = 0;
+        ReaderWay way = new ReaderWay(29L);
+        way.setTag("highway", "service");
+        way.setTag("maxspeed", "3 mph");
+        parser.handleWayTags(edgeId, edgeIntAccess, way, relFlags);
+        assertEquals(4, maxSpeedEnc.getDecimal(false, edgeId, edgeIntAccess), .1);
+    }
+
+    @Test
+    public void parseMaxspeedString() {
+        assertEquals(40, OSMMaxSpeedParser.parseMaxspeedString("40 km/h"), 0.1);
+        assertEquals(40, OSMMaxSpeedParser.parseMaxspeedString("40km/h"), 0.1);
+        assertEquals(40, OSMMaxSpeedParser.parseMaxspeedString("40kmh"), 0.1);
+        assertEquals(64.4, OSMMaxSpeedParser.parseMaxspeedString("40mph"), 0.1);
+        assertEquals(48.3, OSMMaxSpeedParser.parseMaxspeedString("30 mph"), 0.1);
+        assertEquals(18.5, OSMMaxSpeedParser.parseMaxspeedString("10 knots"), 0.1);
+        assertEquals(19, OSMMaxSpeedParser.parseMaxspeedString("19 kph"), 0.1);
+        assertEquals(19, OSMMaxSpeedParser.parseMaxspeedString("19kph"), 0.1);
+        assertEquals(100, OSMMaxSpeedParser.parseMaxspeedString("100"), 0.1);
+        assertEquals(100.5, OSMMaxSpeedParser.parseMaxspeedString("100.5"), 0.1);
+        assertEquals(4.8, OSMMaxSpeedParser.parseMaxspeedString("3 mph"), 0.1);
+
+        assertEquals(OSMMaxSpeedParser.MAXSPEED_NONE, OSMMaxSpeedParser.parseMaxspeedString("none"), 0.1);
+    }
+
+    @Test
+    public void parseMaxspeedString_invalid() {
+        assertEquals(MaxSpeed.MAXSPEED_MISSING, OSMMaxSpeedParser.parseMaxspeedString(null));
+        assertEquals(MaxSpeed.MAXSPEED_MISSING, OSMMaxSpeedParser.parseMaxspeedString("-20"));
+        assertEquals(MaxSpeed.MAXSPEED_MISSING, OSMMaxSpeedParser.parseMaxspeedString("0"));
+        assertEquals(MaxSpeed.MAXSPEED_MISSING, OSMMaxSpeedParser.parseMaxspeedString("1"));
+        assertEquals(MaxSpeed.MAXSPEED_MISSING, OSMMaxSpeedParser.parseMaxspeedString("1km/h"));
+        assertEquals(MaxSpeed.MAXSPEED_MISSING, OSMMaxSpeedParser.parseMaxspeedString("1mph"));
+        assertEquals(MaxSpeed.MAXSPEED_MISSING, OSMMaxSpeedParser.parseMaxspeedString("2"));
+        assertEquals(MaxSpeed.MAXSPEED_MISSING, OSMMaxSpeedParser.parseMaxspeedString("3"));
+        assertEquals(MaxSpeed.MAXSPEED_MISSING, OSMMaxSpeedParser.parseMaxspeedString("4"));
+    }
+
 }
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 1572afcdf65..acc8c076b06 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
@@ -82,30 +82,6 @@ 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(100, OSMValueExtractor.stringToKmh("100"), DELTA);
-        assertEquals(100.5, OSMValueExtractor.stringToKmh("100.5"), 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("0.0")));
-        assertTrue(Double.isNaN(OSMValueExtractor.stringToKmh("-20")));
-    }
-
     @Test
     public void testConditionalWeightToTons() {
         assertEquals(7.5, conditionalWeightToTons("no @ (weight>7.5)"));