Repository: graphhopper/graphhopper
Pull request #1718 (2019-09-09 10:59:40)

make parsing in prepareName for point_hint faster
Improves #1717


Patch:
diff --git a/config-example.yml b/config-example.yml
index c056ec491..d8c2b07ad 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,toll,track_type
+  # More options are: surface,max_width,max_height,max_weight,max_axle_load,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 88ed8a68e..a1751f321 100644
--- a/core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java
+++ b/core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java
@@ -49,6 +49,8 @@ public class DefaultEncodedValueFactory implements EncodedValueFactory {
             enc = MaxHeight.create();
         } else if (MaxWidth.KEY.equals(name)) {
             enc = MaxWidth.create();
+        } else if (MaxAxleLoad.KEY.equals(name)) {
+            enc = MaxAxleLoad.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/MaxAxleLoad.java b/core/src/main/java/com/graphhopper/routing/profiles/MaxAxleLoad.java
new file mode 100644
index 000000000..2872c1947
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/profiles/MaxAxleLoad.java
@@ -0,0 +1,34 @@
+/*
+ *  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;
+
+/**
+ * Defines the maximum weight per axle for an edge.
+ */
+public class MaxAxleLoad {
+    public static final String KEY = "max_axle_load";
+
+    /**
+     * Currently enables to store 0.5 to max=0.5*2⁷ tons and infinity. If a value is between the maximum and infinity
+     * it is assumed to use the maximum value. To save bits it might make more sense to store only a few values like
+     * it was done with the MappedDecimalEncodedValue still handling (or rounding) of unknown values is unclear.
+     */
+    public static DecimalEncodedValue create() {
+        return new UnsignedDecimalEncodedValue(KEY, 7, 0.5, Double.POSITIVE_INFINITY, false);
+    }
+}
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 81ba6a36f..f233015e7 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
@@ -48,6 +48,8 @@ public class DefaultTagParserFactory implements TagParserFactory {
             return new OSMMaxHeightParser();
         else if (name.equals(MaxWidth.KEY))
             return new OSMMaxWidthParser();
+        else if (name.equals(MaxAxleLoad.KEY))
+            return new OSMMaxAxleLoadParser();
         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
new file mode 100644
index 000000000..a07d1cb5f
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxAxleLoadParser.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.MaxAxleLoad;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.routing.util.parsers.helpers.OSMWeightExtractor;
+import com.graphhopper.storage.IntsRef;
+
+public class OSMMaxAxleLoadParser implements TagParser {
+    
+    private final DecimalEncodedValue maxAxleLoadEncoder;
+    private final boolean enableLog;
+
+    public OSMMaxAxleLoadParser() {
+        this(MaxAxleLoad.create(), false);
+    }
+
+    public OSMMaxAxleLoadParser(DecimalEncodedValue maxAxleLoadEncoder, boolean enableLog) {
+        this.maxAxleLoadEncoder = maxAxleLoadEncoder;
+        this.enableLog = enableLog;
+    }
+
+    @Override
+    public void createEncodedValues(EncodedValueLookup lookup,
+                    List<EncodedValue> registerNewEncodedValue) {
+        registerNewEncodedValue.add(maxAxleLoadEncoder);
+    }
+
+    @Override
+    public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay way, EncodingManager.Access access,
+                    long relationFlags) {
+        OSMWeightExtractor.extractTons(edgeFlags, way, maxAxleLoadEncoder,
+                        Collections.singletonList("maxaxleload"), 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 41f240646..e4cdd9702 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
@@ -17,27 +17,22 @@
  */
 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.MaxWeight;
 import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.routing.util.parsers.helpers.OSMWeightExtractor;
 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 OSMMaxWeightParser implements TagParser {
 
-    private static Logger LOG = LoggerFactory.getLogger(OSMMaxWeightParser.class);
-    private final DecimalEncodedValue weightEncoder;
-    private final boolean enableLog;
+    private DecimalEncodedValue weightEncoder;
+    private boolean enableLog;
 
     public OSMMaxWeightParser() {
         this(MaxWeight.create(), false);
@@ -57,38 +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");
-        extractTons(edgeFlags, way, weightEncoder, weightTags, enableLog);
+        OSMWeightExtractor.extractTons(edgeFlags, way, weightEncoder, weightTags, enableLog);
         return edgeFlags;
     }
-
-    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;
-    }
 }
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
new file mode 100644
index 000000000..c72e4571a
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/helpers/OSMWeightExtractor.java
@@ -0,0 +1,53 @@
+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 57578f290..517b402db 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,7 @@ public class PathDetailsBuilderFactory {
         if (requestedPathDetails.contains(DISTANCE))
             builders.add(new DistanceDetails());
 
-        for (String key : Arrays.asList(MaxSpeed.KEY, MaxWidth.KEY, MaxHeight.KEY, MaxWeight.KEY)) {
+        for (String key : Arrays.asList(MaxSpeed.KEY, MaxWidth.KEY, MaxHeight.KEY, MaxWeight.KEY, MaxAxleLoad.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 c81864b6f..999c72bf4 100644
--- a/core/src/test/java/com/graphhopper/routing/util/DataFlagEncoderTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/DataFlagEncoderTest.java
@@ -1,8 +1,34 @@
 package com.graphhopper.routing.util;
 
+import static com.graphhopper.util.GHUtility.updateDistancesFor;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.Test;
+
 import com.graphhopper.reader.ReaderWay;
-import com.graphhopper.routing.profiles.*;
-import com.graphhopper.routing.util.parsers.*;
+import com.graphhopper.routing.profiles.BooleanEncodedValue;
+import com.graphhopper.routing.profiles.Country;
+import com.graphhopper.routing.profiles.DecimalEncodedValue;
+import com.graphhopper.routing.profiles.EnumEncodedValue;
+import com.graphhopper.routing.profiles.IntEncodedValue;
+import com.graphhopper.routing.profiles.MaxSpeed;
+import com.graphhopper.routing.profiles.RoadAccess;
+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;
+import com.graphhopper.routing.util.parsers.OSMSurfaceParser;
+import com.graphhopper.routing.util.parsers.SpatialRuleParser;
 import com.graphhopper.routing.util.spatialrules.SpatialRule;
 import com.graphhopper.routing.util.spatialrules.SpatialRuleLookup;
 import com.graphhopper.routing.util.spatialrules.countries.GermanySpatialRule;
@@ -16,13 +42,6 @@ import com.graphhopper.util.TranslationMapTest;
 import com.graphhopper.util.shapes.BBox;
 import com.graphhopper.util.shapes.GHPoint;
 import com.graphhopper.util.shapes.Polygon;
-import org.junit.Test;
-
-import java.util.Arrays;
-import java.util.Collections;
-
-import static com.graphhopper.util.GHUtility.updateDistancesFor;
-import static org.junit.Assert.*;
 
 /**
  * @author Peter Karich
@@ -333,27 +352,6 @@ public class DataFlagEncoderTest {
         OSMMaxWidthParser.stringToMeter("height limit 1.5m");
     }
 
-    @Test
-    public void stringToTons() {
-        assertEquals(1.5, OSMMaxWeightParser.stringToTons("1.5"), DELTA);
-        assertEquals(1.5, OSMMaxWeightParser.stringToTons("1.5 t"), DELTA);
-        assertEquals(1.5, OSMMaxWeightParser.stringToTons("1.5   t"), DELTA);
-        assertEquals(1.5, OSMMaxWeightParser.stringToTons("1.5 tons"), DELTA);
-        assertEquals(1.5, OSMMaxWeightParser.stringToTons("1.5 ton"), DELTA);
-        assertEquals(1.5, OSMMaxWeightParser.stringToTons("3306.9 lbs"), DELTA);
-        assertEquals(3, OSMMaxWeightParser.stringToTons("3 T"), DELTA);
-        assertEquals(3, OSMMaxWeightParser.stringToTons("3ton"), DELTA);
-
-        // maximum gross weight
-        assertEquals(6, OSMMaxWeightParser.stringToTons("6t mgw"), DELTA);
-    }
-
-    @Test(expected = NumberFormatException.class)
-    public void stringToTonsException() {
-        // Unexpected values
-        OSMMaxWeightParser.stringToTons("weight limit 1.5t");
-    }
-
     @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/OSMMaxAxleLoadParserTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMMaxAxleLoadParserTest.java
new file mode 100644
index 000000000..d5b76a70d
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMMaxAxleLoadParserTest.java
@@ -0,0 +1,69 @@
+package com.graphhopper.routing.util.parsers;
+
+import static com.graphhopper.routing.util.EncodingManager.Access.WAY;
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.graphhopper.reader.ReaderWay;
+import com.graphhopper.routing.profiles.DecimalEncodedValue;
+import com.graphhopper.routing.profiles.MaxAxleLoad;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.storage.IntsRef;
+
+public class OSMMaxAxleLoadParserTest {
+
+    private EncodingManager em;
+    private DecimalEncodedValue malEnc;
+    private OSMMaxAxleLoadParser parser;
+
+    @Before
+    public void setUp() {
+        parser = new OSMMaxAxleLoadParser();
+        em = new EncodingManager.Builder(4).add(parser).build();
+        malEnc = em.getDecimalEncodedValue(MaxAxleLoad.KEY);
+    }
+
+    @Test
+    public void testSimpleTags() {
+        ReaderWay readerWay = new ReaderWay(1);
+        IntsRef intsRef = em.createEdgeFlags();
+        readerWay.setTag("maxaxleload", "11.5");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(11.5, malEnc.getDecimal(false, intsRef), .01);
+
+        // if value is beyond the maximum then do not use infinity instead fallback to more restrictive maximum
+        intsRef = em.createEdgeFlags();
+        readerWay.setTag("maxaxleload", "80");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(malEnc.getMaxDecimal(), malEnc.getDecimal(false, intsRef), .01);
+    }
+
+    @Test
+    public void testRounding() {
+        ReaderWay readerWay = new ReaderWay(1);
+        IntsRef intsRef = em.createEdgeFlags();
+        readerWay.setTag("maxaxleload", "4.8");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(5.0, malEnc.getDecimal(false, intsRef), .01);
+        
+        intsRef = em.createEdgeFlags();
+        readerWay.setTag("maxaxleload", "3.6");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(3.5, malEnc.getDecimal(false, intsRef), .01);
+        
+        intsRef = em.createEdgeFlags();
+        readerWay.setTag("maxaxleload", "2.4");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(2.5, malEnc.getDecimal(false, intsRef), .01);
+    }
+    
+    @Test
+    public void testNoLimit() {
+        ReaderWay readerWay = new ReaderWay(1);
+        IntsRef intsRef = em.createEdgeFlags();
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(Double.POSITIVE_INFINITY, malEnc.getDecimal(false, intsRef), .01);
+    }
+}
\ 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
new file mode 100644
index 000000000..06cd94f83
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMWeightExtractorTest.java
@@ -0,0 +1,33 @@
+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