Repository: graphhopper/graphhopper
Pull request #1703 (2019-08-28 14:30:01)

New MaxLengthParser
* add support for the [maxlength](https://wiki.openstreetmap.org/wiki/Key:maxlength) tag
* moved shared value parsing logic from OSMMaxWidthParser to `com.graphhopper.routing.util.parsers.helpers.OSMDimensionExtractor`

otbutz: I think we should combine them. How about `OSMValueExtractor` ?
karussell: Sounds good!

Patch:
diff --git a/config-example.yml b/config-example.yml
index d8c2b07ad..3b6273de5 100644
--- a/config-example.yml
+++ b/config-example.yml
@@ -15,7 +15,7 @@ graphhopper:
 
   # Add additional information to every edge. Used for path details.
   # If road_environment is added and elevation is enabled then also a tunnel and bridge interpolation is done, see #798.
-  # More options are: surface,max_width,max_height,max_weight,max_axle_load,toll,track_type
+  # More options are: surface,max_width,max_height,max_weight,max_axle_load,max_length,toll,track_type
   graph.encoded_values: road_class,road_class_link,road_environment,max_speed,road_access
   # If many flag_encoders or encoded_values are used you need to increase bytes_for_flags to 8 or more (multiple of 4)
   graph.bytes_for_flags: 4
diff --git a/core/src/main/java/com/graphhopper/routing/ch/PrepareContractionHierarchies.java b/core/src/main/java/com/graphhopper/routing/ch/PrepareContractionHierarchies.java
index 7ac4645b5..6594f3d37 100644
--- a/core/src/main/java/com/graphhopper/routing/ch/PrepareContractionHierarchies.java
+++ b/core/src/main/java/com/graphhopper/routing/ch/PrepareContractionHierarchies.java
@@ -228,39 +228,38 @@ public class PrepareContractionHierarchies extends AbstractAlgoPreparation imple
         // but has always been like that and changing it would possibly require retuning the contraction parameters
         updatePrioritiesOfRemainingNodes();
         nodeContractor.prepareContraction();
-        int initSize = sortedNodes.getSize();
+        final int initSize = sortedNodes.getSize();
         int level = 0;
         checkCounter = 0;
-        long logSize = Math.round(Math.max(10, initSize / 100d * params.getLogMessagesPercentage()));
-        if (params.getLogMessagesPercentage() == 0)
-            logSize = Integer.MAX_VALUE;
-
-        // preparation takes longer but queries are slightly faster with preparation
-        // => enable it but call not so often
-        boolean periodicUpdate = true;
+        final long logSize = params.getLogMessagesPercentage() == 0
+                ? Long.MAX_VALUE
+                : Math.round(Math.max(10, initSize * params.getLogMessagesPercentage() / 100d));
+
+        // specifies after how many contracted nodes the queue of remaining nodes is rebuilt. this takes time but the
+        // more often we do this the more up-to-date the node priorities will be
+        // todo: instead of using a fixed interval size maybe try adjusting it depending on the number of remaining
+        // nodes ?
+        final long periodicUpdatesCount = params.getPeriodicUpdatesPercentage() == 0
+                ? Long.MAX_VALUE
+                : Math.round(Math.max(10, initSize * params.getPeriodicUpdatesPercentage() / 100d));
         int updateCounter = 0;
-        long periodicUpdatesCount = Math.round(Math.max(10, sortedNodes.getSize() / 100d * params.getPeriodicUpdatesPercentage()));
-        if (params.getPeriodicUpdatesPercentage() == 0)
-            periodicUpdate = false;
 
-        // disable lazy updates for last x percentage of nodes as preparation is then a lot slower
-        // and query time does not really benefit
-        long lastNodesLazyUpdates = Math.round(sortedNodes.getSize() / 100d * params.getLastNodesLazyUpdatePercentage());
+        // enable lazy updates for last x percentage of nodes. lazy updates make preparation slower but potentially
+        // keep node priorities more up to date, possibly resulting in a better preparation.
+        final long lastNodesLazyUpdates = Math.round(initSize * params.getLastNodesLazyUpdatePercentage() / 100d);
 
         // according to paper "Polynomial-time Construction of Contraction Hierarchies for Multi-criteria Objectives" by Funke and Storandt
         // we don't need to wait for all nodes to be contracted
-        long nodesToAvoidContract = Math.round((100 - params.getNodesContractedPercentage()) / 100d * sortedNodes.getSize());
+        final long nodesToAvoidContract = Math.round(initSize * (100 - params.getNodesContractedPercentage()) / 100d);
 
-        // Recompute priority of uncontracted neighbors.
-        // Without neighbor updates preparation is faster but we need them
-        // to slightly improve query time. Also if not applied too often it decreases the shortcut number.
-        boolean neighborUpdate = true;
-        if (params.getNeighborUpdatePercentage() == 0)
-            neighborUpdate = false;
+        // Recompute priority of (the given percentage of) uncontracted neighbors. Doing neighbor updates takes additional
+        // time during preparation but keeps node priorities more up to date. this potentially improves query time and
+        // reduces number of shortcuts.
+        final boolean neighborUpdate = (params.getNeighborUpdatePercentage() != 0);
 
         while (!sortedNodes.isEmpty()) {
             // periodically update priorities of ALL nodes
-            if (periodicUpdate && checkCounter > 0 && checkCounter % periodicUpdatesCount == 0) {
+            if (checkCounter > 0 && checkCounter % periodicUpdatesCount == 0) {
                 updatePrioritiesOfRemainingNodes();
                 updateCounter++;
                 if (sortedNodes.isEmpty())
@@ -295,7 +294,7 @@ public class PrepareContractionHierarchies extends AbstractAlgoPreparation imple
                 break;
 
             // there might be multiple edges going to the same neighbor nodes -> only calculate priority once per node
-            IntSet updatedNeighors = new IntHashSet(10);
+            IntSet updatedNeighbors = new IntHashSet(10);
             CHEdgeIterator iter = vehicleAllExplorer.setBaseNode(polledNode);
             while (iter.next()) {
 
@@ -307,13 +306,13 @@ public class PrepareContractionHierarchies extends AbstractAlgoPreparation imple
                 if (prepareGraph.getLevel(nn) != maxLevel)
                     continue;
 
-                if (neighborUpdate && !updatedNeighors.contains(nn) && rand.nextInt(100) < params.getNeighborUpdatePercentage()) {
+                if (neighborUpdate && !updatedNeighbors.contains(nn) && rand.nextInt(100) < params.getNeighborUpdatePercentage()) {
                     neighborUpdateSW.start();
                     float oldPrio = oldPriorities[nn];
                     float priority = oldPriorities[nn] = calculatePriority(nn);
                     if (priority != oldPrio) {
                         sortedNodes.update(nn, oldPrio, priority);
-                        updatedNeighors.add(nn);
+                        updatedNeighbors.add(nn);
                     }
                     neighborUpdateSW.stop();
                 }
@@ -476,26 +475,36 @@ public class PrepareContractionHierarchies extends AbstractAlgoPreparation imple
 
     private static class Params {
         /**
-         * Specifies how often periodic updates will happen. The higher the value the longer the preparation takes
-         * but the less shortcuts are produced.
+         * Specifies after how many contracted nodes a full refresh of the queue of remaining/not contracted nodes
+         * is performed. For example for a graph with 1000 nodes a value of 20 means that a full refresh is performed
+         * after every 200 nodes (20% of the number of nodes of the graph). The more of these updates are performed
+         * the longer the preparation will take, but the more up-to-date the node priorities will be. Higher values
+         * here mean fewer updates!
          */
         private int periodicUpdatesPercentage;
         /**
-         * Specifies when lazy updates will happen, measured relative to all existing nodes. 100 means always.
+         * Specifies the fraction of nodes for which lazy updates will be performed. For example a value of 20 means
+         * that lazy updates will be performed for the last 20% of all nodes. A value of 100 means lazy updates will
+         * be performed for all nodes. Higher values here lead to a longer preparation time, but the node priorities
+         * will be more up-to-date (potentially leading to a better preparation (less shortcuts/faster queries)).
          */
         private int lastNodesLazyUpdatePercentage;
         /**
-         * Specifies how often neighbor updates will happen. 100 means always.
+         * Specifies the probability that the priority of a given neighbor of a contracted node will be updated after
+         * the node was contracted. For example a value of 20 means that on average 20% of the neighbor nodes will be
+         * updated / each neighbor will be updated with a chance of 20%. Higher values here lead to longer preparation
+         * times, but the node priorities will be more up-to-date.
          */
         private int neighborUpdatePercentage;
         /**
-         * Defines how many nodes (percentage) should be contracted. Less nodes means slower query but
-         * faster contraction.
+         * Defines how many nodes (percentage) should be contracted. A value of 20 means only the first 20% of all nodes
+         * will be contracted. Higher values here mean longer preparation times, but faster queries (because the
+         * graph will be fully contracted).
          */
         private int nodesContractedPercentage;
         /**
-         * Specifies how often a log message should be printed. Specify something around 20 (20% of the
-         * start nodes).
+         * Specifies how often a log message should be printed.
+         * @see #periodicUpdatesPercentage
          */
         private int logMessagesPercentage;
 
diff --git a/core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java b/core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java
index a1751f321..e12b6b2a0 100644
--- a/core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java
+++ b/core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java
@@ -51,6 +51,8 @@ public class DefaultEncodedValueFactory implements EncodedValueFactory {
             enc = MaxWidth.create();
         } else if (MaxAxleLoad.KEY.equals(name)) {
             enc = MaxAxleLoad.create();
+        } else if (MaxLength.KEY.equals(name)) {
+            enc = MaxLength.create();
         } else if (Surface.KEY.equals(name)) {
             enc = new EnumEncodedValue<>(Surface.KEY, Surface.class);
         } else if (Toll.KEY.equals(name)) {
diff --git a/core/src/main/java/com/graphhopper/routing/profiles/MaxHeight.java b/core/src/main/java/com/graphhopper/routing/profiles/MaxHeight.java
index bdaa46eb0..b245409bd 100644
--- a/core/src/main/java/com/graphhopper/routing/profiles/MaxHeight.java
+++ b/core/src/main/java/com/graphhopper/routing/profiles/MaxHeight.java
@@ -18,8 +18,9 @@
 package com.graphhopper.routing.profiles;
 
 /**
- * One of the three logistic attributes that can be stored per edge.
+ * One of the four logistic attributes that can be stored per edge.
  *
+ * @see MaxLength
  * @see MaxWidth
  * @see MaxWeight
  */
diff --git a/core/src/main/java/com/graphhopper/routing/profiles/MaxLength.java b/core/src/main/java/com/graphhopper/routing/profiles/MaxLength.java
new file mode 100644
index 000000000..72b7664cd
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/profiles/MaxLength.java
@@ -0,0 +1,37 @@
+/*
+ *  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.profiles;
+
+/**
+ * One of the four logistic attributes that can be stored per edge.
+ *
+ * @see MaxHeight
+ * @see MaxWeight
+ * @see MaxWidth
+ */
+public class MaxLength {
+    public static final String KEY = "max_length";
+
+    /**
+     * Currently enables to store 0.1 to max=0.1*2⁷m and infinity. If a value is
+     * between the maximum and infinity it is assumed to use the maximum value.
+     */
+    public static DecimalEncodedValue create() {
+        return new UnsignedDecimalEncodedValue(KEY, 7, 0.1, Double.POSITIVE_INFINITY, false);
+    }
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/graphhopper/routing/profiles/MaxWeight.java b/core/src/main/java/com/graphhopper/routing/profiles/MaxWeight.java
index caad9a337..3b2470610 100644
--- a/core/src/main/java/com/graphhopper/routing/profiles/MaxWeight.java
+++ b/core/src/main/java/com/graphhopper/routing/profiles/MaxWeight.java
@@ -18,9 +18,10 @@
 package com.graphhopper.routing.profiles;
 
 /**
- * One of the three logistic attributes that can be stored per edge.
+ * One of the four logistic attributes that can be stored per edge.
  *
  * @see MaxHeight
+ * @see MaxLength
  * @see MaxWidth
  */
 public class MaxWeight {
diff --git a/core/src/main/java/com/graphhopper/routing/profiles/MaxWidth.java b/core/src/main/java/com/graphhopper/routing/profiles/MaxWidth.java
index 02d0c6a71..b4d6175ec 100644
--- a/core/src/main/java/com/graphhopper/routing/profiles/MaxWidth.java
+++ b/core/src/main/java/com/graphhopper/routing/profiles/MaxWidth.java
@@ -18,9 +18,10 @@
 package com.graphhopper.routing.profiles;
 
 /**
- * One of the three logistic attributes that can be stored per edge.
+ * One of the four logistic attributes that can be stored per edge.
  *
  * @see MaxHeight
+ * @see MaxLength
  * @see MaxWeight
  */
 public class MaxWidth {
diff --git a/core/src/main/java/com/graphhopper/routing/util/NameSimilarityEdgeFilter.java b/core/src/main/java/com/graphhopper/routing/util/NameSimilarityEdgeFilter.java
index c361b02da..145388953 100644
--- a/core/src/main/java/com/graphhopper/routing/util/NameSimilarityEdgeFilter.java
+++ b/core/src/main/java/com/graphhopper/routing/util/NameSimilarityEdgeFilter.java
@@ -21,25 +21,29 @@ import com.graphhopper.apache.commons.lang3.StringUtils;
 import com.graphhopper.debatty.java.stringsimilarity.JaroWinkler;
 import com.graphhopper.util.EdgeIteratorState;
 
-import java.util.*;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import static com.graphhopper.util.Helper.toLowerCase;
 
 /**
- * This class defines the basis for NameSimilarity matching using an EdgeFilter.
+ * This class defines the basis for NameSimilarity matching using an EdgeFilter. It is not thread-safe.
  * The typical use-case is to match not the nearest edge in
  * {@link com.graphhopper.storage.index.LocationIndex#findClosest(double, double, EdgeFilter)}
- * but the match the edge which name is closest to the pointHint
+ * but the edge with the name that is similar to the specified pointHint and still close.
  * <p>
  * Names that are similar to each other are (n1 name1, n2 name2):
  * <ul>
  * <li>n1 == n2</li>
  * <li>n1 is significant substring of n2, e.g: n1="Main Road", n2="Main Road, New York"</li>
- * <li>n1 and n2 contain a reasonable longest common substring, e.g.: n1="Cape Point / Cape of Good Hope", n2="Cape Point Rd, Cape Peninsula, Cape Town, 8001, Afrique du Sud"</li>
+ * <li>n1 and n2 contain a reasonable longest common substring, e.g.: n1="Cape Point / Cape of Good Hope",
+ *     n2="Cape Point Rd, Cape Peninsula, Cape Town, 8001, Afrique du Sud"</li>
  * </ul>
  * <p>
- * We aim for allowing slight typos/differences of the substrings, without having too much false positives.
+ * The aim is to allow minor typos/differences of the substrings, without having too much false positives.
  *
  * @author Robin Boldt
  * @author Peter Karich
@@ -74,7 +78,7 @@ public class NameSimilarityEdgeFilter implements EdgeFilter {
         put("w", "west");
         put("e", "east");
     }};
-    private static final Pattern NON_WORD_CHAR = Pattern.compile("[^\\p{LD}]+");
+    private static final Pattern WORD_CHAR = Pattern.compile("\\p{LD}+");
     private static final JaroWinkler jaroWinkler = new JaroWinkler();
     private static final double JARO_WINKLER_ACCEPT_FACTOR = .9;
     private final EdgeFilter edgeFilter;
@@ -86,7 +90,7 @@ public class NameSimilarityEdgeFilter implements EdgeFilter {
     }
 
     /**
-     * @param rewriteMap maps abreviations to its longer form
+     * @param rewriteMap maps abbreviations to its longer form
      */
     public NameSimilarityEdgeFilter(EdgeFilter edgeFilter, String pointHint, Map<String, String> rewriteMap) {
         this.edgeFilter = edgeFilter;
@@ -103,33 +107,30 @@ public class NameSimilarityEdgeFilter implements EdgeFilter {
      * TODO Currently limited to certain 'western' languages
      */
     private String prepareName(String name) {
-        // \s = A whitespace character: [ \t\n\x0B\f\r]
-        String[] arr = name.split("\\s");
-        List<String> list = new ArrayList<>(arr.length);
-        for (int i = 0; i < arr.length; i++) {
-            String rewrite = NON_WORD_CHAR.matcher(toLowerCase(arr[i])).replaceAll("");
-            String tmp = rewriteMap.get(rewrite);
-            if (tmp != null)
-                rewrite = tmp;
-            boolean isNumber = false;
-            try {
-                if (Integer.parseInt(rewrite) > 0)
-                    isNumber = true;
-            } catch (NumberFormatException ex) {
-            }
-            // Ignore matching short frases like de, la, ... except it is a number
-            if (rewrite.length() > 2 || isNumber) {
-                list.add(rewrite);
+        StringBuilder sb = new StringBuilder(name.length());
+        Matcher wordCharMatcher = WORD_CHAR.matcher(name);
+        while (wordCharMatcher.find()) {
+            String normalizedToken = toLowerCase(wordCharMatcher.group());
+            String rewrite = rewriteMap.get(normalizedToken);
+            if (rewrite != null)
+                normalizedToken = rewrite;
+            if (normalizedToken.isEmpty())
+                continue;
+            // Ignore matching short phrases like de, la, ... except it is a number
+            if (normalizedToken.length() > 2) {
+                sb.append(normalizedToken);
+            } else {
+                if (Character.isDigit(normalizedToken.charAt(0)) && (normalizedToken.length() == 1 || Character.isDigit(normalizedToken.charAt(1)))) {
+                    sb.append(normalizedToken);
+                }
             }
         }
-        return listToString(list);
+        return sb.toString();
     }
 
     private String removeRelation(String edgeName) {
-        if (edgeName != null && edgeName.contains(", ")) {
-            edgeName = edgeName.substring(0, edgeName.lastIndexOf(','));
-        }
-        return edgeName;
+        int index = edgeName.lastIndexOf(", ");
+        return index >= 0 ? edgeName.substring(0, index) : edgeName;
     }
 
     @Override
@@ -149,7 +150,6 @@ public class NameSimilarityEdgeFilter implements EdgeFilter {
 
         name = removeRelation(name);
         String edgeName = prepareName(name);
-
         return isJaroWinklerSimilar(pointHint, edgeName);
     }
 
@@ -159,14 +159,6 @@ public class NameSimilarityEdgeFilter implements EdgeFilter {
         return jwSimilarity > JARO_WINKLER_ACCEPT_FACTOR;
     }
 
-    private final String listToString(List<String> list) {
-        StringBuilder b = new StringBuilder();
-        for (int i = 0; i < list.size(); i++) {
-            b.append(list.get(i));
-        }
-        return b.toString();
-    }
-
     private boolean isLevenshteinSimilar(String hint, String name) {
         // too big length difference
         if (Math.min(name.length(), hint.length()) * 4 < Math.max(name.length(), hint.length()))
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/DefaultTagParserFactory.java b/core/src/main/java/com/graphhopper/routing/util/parsers/DefaultTagParserFactory.java
index f233015e7..3dcb28530 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/DefaultTagParserFactory.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/DefaultTagParserFactory.java
@@ -50,6 +50,8 @@ public class DefaultTagParserFactory implements TagParserFactory {
             return new OSMMaxWidthParser();
         else if (name.equals(MaxAxleLoad.KEY))
             return new OSMMaxAxleLoadParser();
+        else if (name.equals(MaxLength.KEY))
+            return new OSMMaxLengthParser();
         else if (name.equals(Surface.KEY))
             return new OSMSurfaceParser();
         else if (name.equals(Toll.KEY))
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxAxleLoadParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxAxleLoadParser.java
index a07d1cb5f..0507c7187 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxAxleLoadParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxAxleLoadParser.java
@@ -26,7 +26,7 @@ import com.graphhopper.routing.profiles.EncodedValue;
 import com.graphhopper.routing.profiles.EncodedValueLookup;
 import com.graphhopper.routing.profiles.MaxAxleLoad;
 import com.graphhopper.routing.util.EncodingManager;
-import com.graphhopper.routing.util.parsers.helpers.OSMWeightExtractor;
+import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
 import com.graphhopper.storage.IntsRef;
 
 public class OSMMaxAxleLoadParser implements TagParser {
@@ -52,7 +52,7 @@ public class OSMMaxAxleLoadParser implements TagParser {
     @Override
     public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay way, EncodingManager.Access access,
                     long relationFlags) {
-        OSMWeightExtractor.extractTons(edgeFlags, way, maxAxleLoadEncoder,
+        OSMValueExtractor.extractTons(edgeFlags, way, maxAxleLoadEncoder,
                         Collections.singletonList("maxaxleload"), enableLog);
         return edgeFlags;
     }
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxHeightParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxHeightParser.java
index 509d1c702..53efaf0ad 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxHeightParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxHeightParser.java
@@ -23,6 +23,7 @@ import com.graphhopper.routing.profiles.EncodedValue;
 import com.graphhopper.routing.profiles.EncodedValueLookup;
 import com.graphhopper.routing.profiles.MaxHeight;
 import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
 import com.graphhopper.storage.IntsRef;
 
 import java.util.Arrays;
@@ -50,7 +51,7 @@ public class OSMMaxHeightParser implements TagParser {
     @Override
     public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay way, EncodingManager.Access access, long relationFlags) {
         List<String> heightTags = Arrays.asList("maxheight", "maxheight:physical"/*, the OSM tag "height" is not used for the height of a road, so omit it here! */);
-        OSMMaxWidthParser.extractMeter(edgeFlags, way, heightEncoder, heightTags, enableLog);
+        OSMValueExtractor.extractMeter(edgeFlags, way, heightEncoder, heightTags, enableLog);
         return edgeFlags;
     }
 }
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxLengthParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxLengthParser.java
new file mode 100644
index 000000000..47ee5e09a
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxLengthParser.java
@@ -0,0 +1,59 @@
+/*
+ *  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.parsers;
+
+import java.util.Collections;
+import java.util.List;
+
+import com.graphhopper.reader.ReaderWay;
+import com.graphhopper.routing.profiles.DecimalEncodedValue;
+import com.graphhopper.routing.profiles.EncodedValue;
+import com.graphhopper.routing.profiles.EncodedValueLookup;
+import com.graphhopper.routing.profiles.MaxLength;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
+import com.graphhopper.storage.IntsRef;
+
+public class OSMMaxLengthParser implements TagParser {
+
+    private final DecimalEncodedValue lengthEncoder;
+    private final boolean enableLog;
+
+    public OSMMaxLengthParser() {
+        this(MaxLength.create(), false);
+    }
+
+    public OSMMaxLengthParser(DecimalEncodedValue lengthEncoder, boolean enableLog) {
+        this.lengthEncoder = lengthEncoder;
+        this.enableLog = enableLog;
+    }
+
+    @Override
+    public void createEncodedValues(EncodedValueLookup lookup,
+                    List<EncodedValue> registerNewEncodedValue) {
+        registerNewEncodedValue.add(lengthEncoder);
+    }
+
+    @Override
+    public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay way, EncodingManager.Access access,
+                    long relationFlags) {
+        OSMValueExtractor.extractMeter(edgeFlags, way, lengthEncoder,
+                        Collections.singletonList("maxlength"), enableLog);
+        return edgeFlags;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxWeightParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxWeightParser.java
index e4cdd9702..bcf20522a 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxWeightParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxWeightParser.java
@@ -26,7 +26,7 @@ import com.graphhopper.routing.profiles.EncodedValue;
 import com.graphhopper.routing.profiles.EncodedValueLookup;
 import com.graphhopper.routing.profiles.MaxWeight;
 import com.graphhopper.routing.util.EncodingManager;
-import com.graphhopper.routing.util.parsers.helpers.OSMWeightExtractor;
+import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
 import com.graphhopper.storage.IntsRef;
 
 public class OSMMaxWeightParser implements TagParser {
@@ -52,7 +52,7 @@ public class OSMMaxWeightParser implements TagParser {
     public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay way, EncodingManager.Access access, long relationFlags) {
         // do not include OSM tag "height" here as it has completely different meaning (height of peak)
         List<String> weightTags = Arrays.asList("maxweight", "maxgcweight");
-        OSMWeightExtractor.extractTons(edgeFlags, way, weightEncoder, weightTags, enableLog);
+        OSMValueExtractor.extractTons(edgeFlags, way, weightEncoder, weightTags, enableLog);
         return edgeFlags;
     }
 }
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxWidthParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxWidthParser.java
index a1da91477..28382f905 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxWidthParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxWidthParser.java
@@ -17,25 +17,20 @@
  */
 package com.graphhopper.routing.util.parsers;
 
+import java.util.Arrays;
+import java.util.List;
+
 import com.graphhopper.reader.ReaderWay;
 import com.graphhopper.routing.profiles.DecimalEncodedValue;
 import com.graphhopper.routing.profiles.EncodedValue;
 import com.graphhopper.routing.profiles.EncodedValueLookup;
 import com.graphhopper.routing.profiles.MaxWidth;
 import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
 import com.graphhopper.storage.IntsRef;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Arrays;
-import java.util.List;
-
-import static com.graphhopper.util.Helper.isEmpty;
-import static com.graphhopper.util.Helper.toLowerCase;
 
 public class OSMMaxWidthParser implements TagParser {
 
-    private static Logger LOG = LoggerFactory.getLogger(OSMMaxWidthParser.class);
     private final DecimalEncodedValue widthEncoder;
     private final boolean enableLog;
 
@@ -56,63 +51,7 @@ public class OSMMaxWidthParser implements TagParser {
     @Override
     public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay way, EncodingManager.Access access, long relationFlags) {
         List<String> widthTags = Arrays.asList("maxwidth", "maxwidth:physical", "width");
-        extractMeter(edgeFlags, way, widthEncoder, widthTags, enableLog);
+        OSMValueExtractor.extractMeter(edgeFlags, way, widthEncoder, widthTags, enableLog);
         return edgeFlags;
     }
-
-    static void extractMeter(IntsRef edgeFlags, ReaderWay way, DecimalEncodedValue valueEncoder, List<String> keys, boolean enableLog) {
-        String value = way.getFirstPriorityTag(keys);
-        if (isEmpty(value))
-            return;
-        try {
-            double val = stringToMeter(value);
-            if (val > valueEncoder.getMaxDecimal())
-                val = valueEncoder.getMaxDecimal();
-            valueEncoder.setDecimal(false, edgeFlags, val);
-        } catch (Exception ex) {
-            if (enableLog)
-                LOG.warn("Unable to extract meter from malformed road attribute '{}' for way (OSM_ID = {}).", value, way.getId());
-        }
-    }
-
-    public static double stringToMeter(String value) {
-        value = toLowerCase(value).replaceAll(" ", "").replaceAll("(meters|meter|mtrs|mtr|mt|m\\.)", "m");
-        double factor = 1;
-        double offset = 0;
-        value = value.replaceAll("(\\\"|\'\')", "in").replaceAll("(\'|feet)", "ft");
-        if (value.startsWith("~") || value.contains("approx")) {
-            value = value.replaceAll("(\\~|approx)", "").trim();
-            factor = 0.8;
-        }
-
-        if (value.equals("default") || value.equals("none") || value.equals("unsigned"))
-            return -1;
-
-        if (value.endsWith("in")) {
-            int startIndex = value.indexOf("ft");
-            String inchValue;
-            if (startIndex < 0) {
-                startIndex = 0;
-            } else {
-                startIndex += 2;
-            }
-
-            inchValue = value.substring(startIndex, value.length() - 2);
-            value = value.substring(0, startIndex);
-            offset = Double.parseDouble(inchValue) * 0.0254;
-        }
-
-        if (value.endsWith("ft")) {
-            value = value.substring(0, value.length() - 2);
-            factor *= 0.3048;
-        } else if (value.endsWith("m")) {
-            value = value.substring(0, value.length() - 1);
-        }
-
-        if (value.isEmpty()) {
-            return offset;
-        } else {
-            return Double.parseDouble(value) * factor + offset;
-        }
-    }
 }
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
new file mode 100644
index 000000000..ee0174c1d
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/helpers/OSMValueExtractor.java
@@ -0,0 +1,109 @@
+package com.graphhopper.routing.util.parsers.helpers;
+
+import static com.graphhopper.util.Helper.isEmpty;
+import static com.graphhopper.util.Helper.toLowerCase;
+
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.graphhopper.reader.ReaderWay;
+import com.graphhopper.routing.profiles.DecimalEncodedValue;
+import com.graphhopper.storage.IntsRef;
+
+public class OSMValueExtractor {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(OSMValueExtractor.class);
+
+    private OSMValueExtractor() {
+        // utility class
+    }
+
+    public static void extractTons(IntsRef edgeFlags, ReaderWay way, DecimalEncodedValue valueEncoder, List<String> keys, boolean enableLog) {
+        String value = way.getFirstPriorityTag(keys);
+        if (isEmpty(value))
+            return;
+        try {
+            double val = stringToTons(value);
+            if (val > valueEncoder.getMaxDecimal())
+                val = valueEncoder.getMaxDecimal();
+            valueEncoder.setDecimal(false, edgeFlags, val);
+        } catch (Exception ex) {
+            if (enableLog)
+                LOG.warn("Unable to extract tons from malformed road attribute '{}' for way (OSM_ID = {}).", value, way.getId());
+        }
+    }
+
+    public static double stringToTons(String value) {
+        value = toLowerCase(value).replaceAll(" ", "").replaceAll("(tons|ton)", "t");
+        value = value.replace("mgw", "").trim();
+        double factor = 1;
+        if (value.equals("default") || value.equals("none")) {
+            return -1;
+        } else if (value.endsWith("t")) {
+            value = value.substring(0, value.length() - 1);
+        } else if (value.endsWith("lbs")) {
+            value = value.substring(0, value.length() - 3);
+            factor = 0.00045359237;
+        }
+    
+        return Double.parseDouble(value) * factor;
+    }
+
+    public static void extractMeter(IntsRef edgeFlags, ReaderWay way, DecimalEncodedValue valueEncoder, List<String> keys, boolean enableLog) {
+        String value = way.getFirstPriorityTag(keys);
+        if (isEmpty(value))
+            return;
+        try {
+            double val = stringToMeter(value);
+            if (val > valueEncoder.getMaxDecimal())
+                val = valueEncoder.getMaxDecimal();
+            valueEncoder.setDecimal(false, edgeFlags, val);
+        } catch (Exception ex) {
+            if (enableLog)
+                LOG.warn("Unable to extract meter from malformed road attribute '{}' for way (OSM_ID = {}).", value, way.getId());
+        }
+    }
+
+    public static double stringToMeter(String value) {
+        value = toLowerCase(value).replaceAll(" ", "").replaceAll("(meters|meter|mtrs|mtr|mt|m\\.)", "m");
+        double factor = 1;
+        double offset = 0;
+        value = value.replaceAll("(\\\"|\'\')", "in").replaceAll("(\'|feet)", "ft");
+        if (value.startsWith("~") || value.contains("approx")) {
+            value = value.replaceAll("(\\~|approx)", "").trim();
+            factor = 0.8;
+        }
+    
+        if (value.equals("default") || value.equals("none") || value.equals("unsigned"))
+            return -1;
+    
+        if (value.endsWith("in")) {
+            int startIndex = value.indexOf("ft");
+            String inchValue;
+            if (startIndex < 0) {
+                startIndex = 0;
+            } else {
+                startIndex += 2;
+            }
+    
+            inchValue = value.substring(startIndex, value.length() - 2);
+            value = value.substring(0, startIndex);
+            offset = Double.parseDouble(inchValue) * 0.0254;
+        }
+    
+        if (value.endsWith("ft")) {
+            value = value.substring(0, value.length() - 2);
+            factor *= 0.3048;
+        } else if (value.endsWith("m")) {
+            value = value.substring(0, value.length() - 1);
+        }
+    
+        if (value.isEmpty()) {
+            return offset;
+        } else {
+            return Double.parseDouble(value) * factor + offset;
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/helpers/OSMWeightExtractor.java b/core/src/main/java/com/graphhopper/routing/util/parsers/helpers/OSMWeightExtractor.java
deleted file mode 100644
index c72e4571a..000000000
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/helpers/OSMWeightExtractor.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.graphhopper.routing.util.parsers.helpers;
-
-import static com.graphhopper.util.Helper.isEmpty;
-import static com.graphhopper.util.Helper.toLowerCase;
-
-import java.util.List;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.graphhopper.reader.ReaderWay;
-import com.graphhopper.routing.profiles.DecimalEncodedValue;
-import com.graphhopper.storage.IntsRef;
-
-public class OSMWeightExtractor {
-    
-    private static final Logger LOG = LoggerFactory.getLogger(OSMWeightExtractor.class);
-
-    private OSMWeightExtractor() {
-        // utility class
-    }
-
-    public static void extractTons(IntsRef edgeFlags, ReaderWay way, DecimalEncodedValue valueEncoder, List<String> keys, boolean enableLog) {
-        String value = way.getFirstPriorityTag(keys);
-        if (isEmpty(value))
-            return;
-        try {
-            double val = OSMWeightExtractor.stringToTons(value);
-            if (val > valueEncoder.getMaxDecimal())
-                val = valueEncoder.getMaxDecimal();
-            valueEncoder.setDecimal(false, edgeFlags, val);
-        } catch (Exception ex) {
-            if (enableLog)
-                LOG.warn("Unable to extract tons from malformed road attribute '{}' for way (OSM_ID = {}).", value, way.getId());
-        }
-    }
-
-    public static double stringToTons(String value) {
-        value = toLowerCase(value).replaceAll(" ", "").replaceAll("(tons|ton)", "t");
-        value = value.replace("mgw", "").trim();
-        double factor = 1;
-        if (value.equals("default") || value.equals("none")) {
-            return -1;
-        } else if (value.endsWith("t")) {
-            value = value.substring(0, value.length() - 1);
-        } else if (value.endsWith("lbs")) {
-            value = value.substring(0, value.length() - 3);
-            factor = 0.00045359237;
-        }
-    
-        return Double.parseDouble(value) * factor;
-    }
-}
\ No newline at end of file
diff --git a/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java b/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
index 517b402db..e50e4cdf8 100644
--- a/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
+++ b/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
@@ -54,7 +54,8 @@ public class PathDetailsBuilderFactory {
         if (requestedPathDetails.contains(DISTANCE))
             builders.add(new DistanceDetails());
 
-        for (String key : Arrays.asList(MaxSpeed.KEY, MaxWidth.KEY, MaxHeight.KEY, MaxWeight.KEY, MaxAxleLoad.KEY)) {
+        for (String key : Arrays.asList(MaxSpeed.KEY, MaxWidth.KEY, MaxHeight.KEY, MaxWeight.KEY,
+                        MaxAxleLoad.KEY, MaxLength.KEY)) {
             if (requestedPathDetails.contains(key) && encoder.hasEncodedValue(key))
                 builders.add(new DecimalDetails(key, encoder.getDecimalEncodedValue(key)));
         }
diff --git a/tools/src/main/java/com/graphhopper/tools/Measurement.java b/tools/src/main/java/com/graphhopper/tools/Measurement.java
index c10e355fa..e7b331607 100644
--- a/tools/src/main/java/com/graphhopper/tools/Measurement.java
+++ b/tools/src/main/java/com/graphhopper/tools/Measurement.java
@@ -143,14 +143,16 @@ public class Measurement {
             GHBitSet allowedEdges = printGraphDetails(g, vehicleStr);
             printMiscUnitPerfTests(g, isCH, encoder, count * 100, allowedEdges);
             printLocationIndexQuery(g, hopper.getLocationIndex(), count);
-            printTimeOfRouteQuery(hopper, isCH, isLM, count / 20, "routing", vehicleStr, true, -1, true, false);
+            printTimeOfRouteQuery(hopper, isCH, isLM, count / 20, "routing", vehicleStr,
+                    true, false, -1, true, false);
 
             if (hopper.getLMFactoryDecorator().isEnabled()) {
                 System.gc();
                 isLM = true;
                 int activeLMCount = 12;
                 for (; activeLMCount > 3; activeLMCount -= 4) {
-                    printTimeOfRouteQuery(hopper, isCH, isLM, count / 4, "routingLM" + activeLMCount, vehicleStr, true, activeLMCount, true, false);
+                    printTimeOfRouteQuery(hopper, isCH, isLM, count / 4, "routingLM" + activeLMCount, vehicleStr,
+                            true, false, activeLMCount, true, false);
                 }
 
                 // compareRouting(hopper, vehicleStr, count / 5);
@@ -164,7 +166,8 @@ public class Measurement {
                     System.gc();
                     // try just one constellation, often ~4-6 is best
                     int lmCount = 5;
-                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCHLM" + lmCount, vehicleStr, true, lmCount, true, false);
+                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCHLM" + lmCount, vehicleStr,
+                            true, false, lmCount, true, false);
                 }
 
                 isLM = false;
@@ -174,13 +177,20 @@ public class Measurement {
                     CHGraph lg = g.getCHGraph(chProfile);
                     fillAllowedEdges(lg.getAllEdges(), allowedEdges);
                     printMiscUnitPerfTests(lg, isCH, encoder, count * 100, allowedEdges);
-                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCH", vehicleStr, true, -1, true, false);
-                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCH_no_sod", vehicleStr, true, -1, false, false);
-                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCH_no_instr", vehicleStr, false, -1, true, false);
+                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCH", vehicleStr,
+                            true, false, -1, true, false);
+                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCH_with_hints", vehicleStr,
+                            true, true, -1, true, false);
+                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCH_no_sod", vehicleStr,
+                            true, false, -1, false, false);
+                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCH_no_instr", vehicleStr,
+                            false, false, -1, true, false);
                 }
                 if (!hopper.getCHFactoryDecorator().getEdgeBasedCHProfiles().isEmpty()) {
-                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCH_edge", vehicleStr, true, -1, false, true);
-                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCH_edge_no_instr", vehicleStr, false, -1, false, true);
+                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCH_edge", vehicleStr,
+                            true, false, -1, false, true);
+                    printTimeOfRouteQuery(hopper, isCH, isLM, count, "routingCH_edge_no_instr", vehicleStr,
+                            false, false, -1, false, true);
                 }
             }
         } catch (Exception ex) {
@@ -407,7 +417,8 @@ public class Measurement {
 
     private void printTimeOfRouteQuery(final GraphHopper hopper, final boolean ch, final boolean lm,
                                        int count, String prefix, final String vehicle,
-                                       final boolean withInstructions, final int activeLandmarks, final boolean sod, final boolean edgeBased) {
+                                       final boolean withInstructions, final boolean withPointHints,
+                                       final int activeLandmarks, final boolean sod, final boolean edgeBased) {
         final Graph g = hopper.getGraphHopperStorage();
         final AtomicLong maxDistance = new AtomicLong(0);
         final AtomicLong minDistance = new AtomicLong(Long.MAX_VALUE);
@@ -416,6 +427,7 @@ public class Measurement {
         final AtomicInteger failedCount = new AtomicInteger(0);
         final DistanceCalc distCalc = new DistanceCalcEarth();
 
+        final EdgeExplorer edgeExplorer = g.createEdgeExplorer(DefaultEdgeFilter.allEdges(hopper.getEncodingManager().getEncoder(vehicle)));
         final AtomicLong visitedNodesSum = new AtomicLong(0);
 //        final AtomicLong extractTimeSum = new AtomicLong(0);
 //        final AtomicLong calcPointsTimeSum = new AtomicLong(0);
@@ -447,6 +459,17 @@ public class Measurement {
                 if (withInstructions)
                     req.setPathDetails(Arrays.asList(Parameters.Details.AVERAGE_SPEED));
 
+
+                if (withPointHints) {
+                    EdgeIterator iter = edgeExplorer.setBaseNode(from);
+                    if (!iter.next())
+                        throw new IllegalArgumentException("wrong 'from' when adding point hint");
+                    EdgeIterator iter2 = edgeExplorer.setBaseNode(to);
+                    if (!iter2.next())
+                        throw new IllegalArgumentException("wrong 'to' when adding point hint");
+                    req.setPointHints(Arrays.asList(iter.getName(), iter2.getName()));
+                }
+
                 // put(algo + ".approximation", "BeelineSimplification").
                 // put(algo + ".epsilon", 2);
 


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/util/DataFlagEncoderTest.java b/core/src/test/java/com/graphhopper/routing/util/DataFlagEncoderTest.java
index 999c72bf4..e0b17c6fb 100644
--- a/core/src/test/java/com/graphhopper/routing/util/DataFlagEncoderTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/DataFlagEncoderTest.java
@@ -23,7 +23,6 @@ import com.graphhopper.routing.profiles.RoadClass;
 import com.graphhopper.routing.profiles.RoadEnvironment;
 import com.graphhopper.routing.profiles.Surface;
 import com.graphhopper.routing.util.parsers.OSMMaxSpeedParser;
-import com.graphhopper.routing.util.parsers.OSMMaxWidthParser;
 import com.graphhopper.routing.util.parsers.OSMRoadAccessParser;
 import com.graphhopper.routing.util.parsers.OSMRoadClassParser;
 import com.graphhopper.routing.util.parsers.OSMRoadEnvironmentParser;
@@ -58,8 +57,6 @@ public class DataFlagEncoderTest {
     private final DecimalEncodedValue carMaxSpeedEnc;
     private final EncodingManager encodingManager;
 
-    private final double DELTA = 0.1;
-
     public DataFlagEncoderTest() {
         properties = new PMap();
         encoder = new DataFlagEncoder(properties);
@@ -320,38 +317,6 @@ public class DataFlagEncoderTest {
         assertTrue(encoder.getAccess(osmWay).canSkip());
     }
 
-    @Test
-    public void stringToMeter() {
-        assertEquals(1.5, OSMMaxWidthParser.stringToMeter("1.5"), DELTA);
-        assertEquals(1.5, OSMMaxWidthParser.stringToMeter("1.5m"), DELTA);
-        assertEquals(1.5, OSMMaxWidthParser.stringToMeter("1.5 m"), DELTA);
-        assertEquals(1.5, OSMMaxWidthParser.stringToMeter("1.5   m"), DELTA);
-        assertEquals(1.5, OSMMaxWidthParser.stringToMeter("1.5 meter"), DELTA);
-        assertEquals(1.5, OSMMaxWidthParser.stringToMeter("4 ft 11 in"), DELTA);
-        assertEquals(1.5, OSMMaxWidthParser.stringToMeter("4'11''"), DELTA);
-
-
-        assertEquals(3, OSMMaxWidthParser.stringToMeter("3 m."), DELTA);
-        assertEquals(3, OSMMaxWidthParser.stringToMeter("3meters"), DELTA);
-        assertEquals(0.8 * 3, OSMMaxWidthParser.stringToMeter("~3"), DELTA);
-        assertEquals(3 * 0.8, OSMMaxWidthParser.stringToMeter("3 m approx"), DELTA);
-
-        // 2.743 + 0.178
-        assertEquals(2.921, OSMMaxWidthParser.stringToMeter("9 ft 7in"), DELTA);
-        assertEquals(2.921, OSMMaxWidthParser.stringToMeter("9'7\""), DELTA);
-        assertEquals(2.921, OSMMaxWidthParser.stringToMeter("9'7''"), DELTA);
-        assertEquals(2.921, OSMMaxWidthParser.stringToMeter("9' 7\""), DELTA);
-
-        assertEquals(2.743, OSMMaxWidthParser.stringToMeter("9'"), DELTA);
-        assertEquals(2.743, OSMMaxWidthParser.stringToMeter("9 feet"), DELTA);
-    }
-
-    @Test(expected = NumberFormatException.class)
-    public void stringToMeterException() {
-        // Unexpected values
-        OSMMaxWidthParser.stringToMeter("height limit 1.5m");
-    }
-
     @Test
     public void testSpatialId() {
         final GermanySpatialRule germany = new GermanySpatialRule();
diff --git a/core/src/test/java/com/graphhopper/routing/util/NameSimilarityEdgeFilterTest.java b/core/src/test/java/com/graphhopper/routing/util/NameSimilarityEdgeFilterTest.java
index d9d3250e5..4171b36e1 100644
--- a/core/src/test/java/com/graphhopper/routing/util/NameSimilarityEdgeFilterTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/NameSimilarityEdgeFilterTest.java
@@ -104,6 +104,7 @@ public class NameSimilarityEdgeFilterTest {
 
         assertTrue(createNameSimilarityEdgeFilter("Main Rd").accept(createTestEdgeIterator("Main Road")));
         assertTrue(createNameSimilarityEdgeFilter("Main Road").accept(createTestEdgeIterator("Main Rd")));
+        assertTrue(createNameSimilarityEdgeFilter("Main Rd").accept(createTestEdgeIterator("Main Road, New York")));
 
         assertTrue(createNameSimilarityEdgeFilter("Cape Point Rd").accept(createTestEdgeIterator("Cape Point")));
         assertTrue(createNameSimilarityEdgeFilter("Cape Point Rd").accept(createTestEdgeIterator("Cape Point Road")));
@@ -136,7 +137,8 @@ public class NameSimilarityEdgeFilterTest {
         assertEquals("northderby", createNameSimilarityEdgeFilter("North Derby Lane").getNormalizedPointHint());
 
         // do not remove the number as it is a significant part of the name, especially in the US
-        assertEquals("i28north", createNameSimilarityEdgeFilter("I-28 N").getNormalizedPointHint());
+        assertEquals("28north", createNameSimilarityEdgeFilter("I-28 N").getNormalizedPointHint());
+        assertEquals("28north", createNameSimilarityEdgeFilter(" I-28    N  ").getNormalizedPointHint());
         assertEquals("south23rd", createNameSimilarityEdgeFilter("S 23rd St").getNormalizedPointHint());
         assertEquals("66", createNameSimilarityEdgeFilter("Route 66").getNormalizedPointHint());
         assertEquals("fayettecounty1", createNameSimilarityEdgeFilter("Fayette County Rd 1").getNormalizedPointHint());
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
new file mode 100644
index 000000000..916249b18
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMValueExtractorTest.java
@@ -0,0 +1,65 @@
+package com.graphhopper.routing.util.parsers;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import com.graphhopper.routing.util.parsers.helpers.OSMValueExtractor;
+
+public class OSMValueExtractorTest {
+    
+    private final double DELTA = 0.1;
+
+    @Test
+    public void stringToTons() {
+        assertEquals(1.5, OSMValueExtractor.stringToTons("1.5"), DELTA);
+        assertEquals(1.5, OSMValueExtractor.stringToTons("1.5 t"), DELTA);
+        assertEquals(1.5, OSMValueExtractor.stringToTons("1.5   t"), DELTA);
+        assertEquals(1.5, OSMValueExtractor.stringToTons("1.5 tons"), DELTA);
+        assertEquals(1.5, OSMValueExtractor.stringToTons("1.5 ton"), DELTA);
+        assertEquals(1.5, OSMValueExtractor.stringToTons("3306.9 lbs"), DELTA);
+        assertEquals(3, OSMValueExtractor.stringToTons("3 T"), DELTA);
+        assertEquals(3, OSMValueExtractor.stringToTons("3ton"), DELTA);
+
+        // maximum gross weight
+        assertEquals(6, OSMValueExtractor.stringToTons("6t mgw"), DELTA);
+    }
+
+    @Test(expected = NumberFormatException.class)
+    public void stringToTonsException() {
+        // Unexpected values
+        OSMValueExtractor.stringToTons("weight limit 1.5t");
+    }
+
+    @Test
+    public void stringToMeter() {
+        assertEquals(1.5, OSMValueExtractor.stringToMeter("1.5"), DELTA);
+        assertEquals(1.5, OSMValueExtractor.stringToMeter("1.5m"), DELTA);
+        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(3, OSMValueExtractor.stringToMeter("3 m."), DELTA);
+        assertEquals(3, OSMValueExtractor.stringToMeter("3meters"), DELTA);
+        assertEquals(0.8 * 3, OSMValueExtractor.stringToMeter("~3"), DELTA);
+        assertEquals(3 * 0.8, OSMValueExtractor.stringToMeter("3 m approx"), DELTA);
+
+        // 2.743 + 0.178
+        assertEquals(2.921, OSMValueExtractor.stringToMeter("9 ft 7in"), DELTA);
+        assertEquals(2.921, OSMValueExtractor.stringToMeter("9'7\""), DELTA);
+        assertEquals(2.921, OSMValueExtractor.stringToMeter("9'7''"), DELTA);
+        assertEquals(2.921, OSMValueExtractor.stringToMeter("9' 7\""), DELTA);
+
+        assertEquals(2.743, OSMValueExtractor.stringToMeter("9'"), DELTA);
+        assertEquals(2.743, OSMValueExtractor.stringToMeter("9 feet"), DELTA);
+    }
+
+    @Test(expected = NumberFormatException.class)
+    public void stringToMeterException() {
+        // Unexpected values
+        OSMValueExtractor.stringToMeter("height limit 1.5m");
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMWeightExtractorTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMWeightExtractorTest.java
deleted file mode 100644
index 06cd94f83..000000000
--- a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMWeightExtractorTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.graphhopper.routing.util.parsers;
-
-import static org.junit.Assert.assertEquals;
-
-import org.junit.Test;
-
-import com.graphhopper.routing.util.parsers.helpers.OSMWeightExtractor;
-
-public class OSMWeightExtractorTest {
-    
-    private final double DELTA = 0.1;
-
-    @Test
-    public void stringToTons() {
-        assertEquals(1.5, OSMWeightExtractor.stringToTons("1.5"), DELTA);
-        assertEquals(1.5, OSMWeightExtractor.stringToTons("1.5 t"), DELTA);
-        assertEquals(1.5, OSMWeightExtractor.stringToTons("1.5   t"), DELTA);
-        assertEquals(1.5, OSMWeightExtractor.stringToTons("1.5 tons"), DELTA);
-        assertEquals(1.5, OSMWeightExtractor.stringToTons("1.5 ton"), DELTA);
-        assertEquals(1.5, OSMWeightExtractor.stringToTons("3306.9 lbs"), DELTA);
-        assertEquals(3, OSMWeightExtractor.stringToTons("3 T"), DELTA);
-        assertEquals(3, OSMWeightExtractor.stringToTons("3ton"), DELTA);
-
-        // maximum gross weight
-        assertEquals(6, OSMWeightExtractor.stringToTons("6t mgw"), DELTA);
-    }
-
-    @Test(expected = NumberFormatException.class)
-    public void stringToTonsException() {
-        // Unexpected values
-        OSMWeightExtractor.stringToTons("weight limit 1.5t");
-    }
-}
\ No newline at end of file