Repository: graphhopper/graphhopper
Pull request #1894 (2020-02-10 08:08:44)

Move SpatialRuleLookup benchmark into Measurement class
This should fix the sporadically failing unittests.


Patch:
diff --git a/README.md b/README.md
index cc401dfe0..790394043 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ like finding and fixing bugs and improving our documentation or translations!
 
 ## For the Web
 
-See GraphHopper in action on [GraphHopper Maps](https://graphhopper.com/maps) and [install it](./docs/index.md) on your own machine:
+See GraphHopper in action on [GraphHopper Maps](https://graphhopper.com/maps) and [install it](https://github.com/graphhopper/graphhopper/blob/0.13/docs/index.md) on your own machine:
 
 [![GraphHopper Maps](https://karussell.files.wordpress.com/2014/12/graphhopper-maps-0-4-preview.png)](https://graphhopper.com/maps)
 
@@ -86,9 +86,9 @@ There is an extension that creates a [navigation endpoint](https://github.com/gr
 
 There is the isochrone subproject to calculate and visualize the reachable area for a certain travel mode
 
-**[Isochrone Web API](./docs/web/api-doc.md#isochrone)**
+**[Isochrone Web API]()**
 
-[![Isochrone API image](./docs/isochrone/images/isochrone.png)](./docs/web/api-doc.md#isochrone)
+[![Isochrone API image](./docs/isochrone/images/isochrone.png)](https://github.com/graphhopper/graphhopper/blob/0.13/docs/web/api-doc.md#isochrone)
 
 **[Shortest Path Tree API](//www.graphhopper.com/blog/2018/07/04/high-precision-reachability/)**
 
@@ -149,7 +149,7 @@ Embed GraphHopper with OpenStreetMap support into your Java application via the
 ```
 
 If you want to write your own import procedure or you don't need OSM import like
-on [Android](./docs/android/index.md), then use:
+on [Android](https://github.com/graphhopper/graphhopper/blob/0.13/docs/android/index.md), then use:
 
 ```xml
 <dependency>
@@ -192,19 +192,18 @@ A fast and production ready map visualization for the Desktop can be implemented
 
 Here is a list of the more detailed features including a link to the documentation:
 
- * [Quick installation and start for users](./docs/web/quickstart.md) - just Java necessary! [Simple start for developers](./docs/core/quickstart-from-source.md) due to Maven.
+ * Based on Java and simple start for developers via Maven.
  * Works out of the box with OpenStreetMap (osm/xml and pbf) and can be adapted to custom data
  * OpenStreetMap integration: stores and considers road type, speed limit, the surface, barriers, access restrictions, ferries, [conditional access restrictions](https://github.com/graphhopper/graphhopper/pull/621), ...
  * GraphHopper is fast. And with the so called "Contraction Hierarchies" it can be even faster (enabled by default).
  * Memory efficient data structures, algorithms and [the low and high level API](./docs/core/low-level-api.md) is tuned towards ease of use and efficiency
- * Provides a simple [web API](./docs/web/api-doc.md) including JavaScript and Java clients
+ * Provides a simple [web API](https://github.com/graphhopper/graphhopper/blob/0.13/docs/web/api-doc.md) including JavaScript and Java clients
  * Multiple weightings (fastest/shortest/...) and pre-built routing profiles: car, bike, racingbike, mountain bike, foot, motorcycle, ...
  * Supports public transit routing and [GTFS](./reader-gtfs/README.md).
  * Offers turn instructions in more than 42 languages, contribute or improve [here](./docs/core/translations.md)
  * Displays and takes into account [elevation data](./docs/core/elevation.md) (per default disabled)
  * Can apply [real time changes to edge weights](https://graphhopper.com/blog/2015/04/08/visualize-and-handle-traffic-information-with-graphhopper-in-real-time-for-cologne-germany-koln/) (flexible and hybrid mode only)
  * Customization of vehicle profiles per request are possible (flexible and hybrid mode only)
- * Possibility to specify a [heading parameter](./docs/core/routing.md) of the vehicle for start, end and via points for navigation applications via `pass_through` or `heading` parameters (flexible and hybrid mode only)
  * [Alternative routes](https://discuss.graphhopper.com/t/alternative-routes/424) (flexible and hybrid mode only)
  * [Turn costs and restrictions](./docs/core/turn-restrictions.md)
  * Country specific routing via SpatialRules
@@ -212,6 +211,6 @@ Here is a list of the more detailed features including a link to the documentati
  * Scales from small indoor-sized to world-wide-sized graphs
  * Finds nearest point on street e.g. to get elevation or 'snap to road' or being used as spatial index (see [#1485](https://github.com/graphhopper/graphhopper/pull/1485))
  * Does [map matching](https://github.com/graphhopper/map-matching)
- * Calculates [isochrones](./docs/web/api-doc.md#isochrone) and [shortest path trees](https://github.com/graphhopper/graphhopper/pull/1577)
+ * Calculates isochrones and [shortest path trees](https://github.com/graphhopper/graphhopper/pull/1577)
  * Shows the whole road network in the browser for debugging purposes ("vector tile support") [#1572](https://github.com/graphhopper/graphhopper/pull/1572)
  * Shows details along a route like road_class or max_speed ("path details") [#1142](https://github.com/graphhopper/graphhopper/pull/1142) or for the whole road network via the "vector tile support"
diff --git a/benchmark/benchmark.sh b/benchmark/benchmark.sh
index 0548f7cf7..90f5098dc 100755
--- a/benchmark/benchmark.sh
+++ b/benchmark/benchmark.sh
@@ -1,6 +1,6 @@
 #!/bin/bash
 # usage:
-# benchmark/benchmark.sh <data_dir> <results_dir> <small_osm_map_path> <big_osm_map_path> <use_measurement_time_as_ref_time>
+# benchmark/benchmark.sh <data_dir> <results_dir> <small_osm_map_path> <big_osm_map_path> <use_measurement_time_as_ref_time> <spatial_rule_borders_dir>
 #
 # where:
 # <data_dir> = base directory used to store results
@@ -8,6 +8,7 @@
 # <small_osm_map_path> = path to osm map the measurement is run on for slow measurements
 # <big_osm_map_path> = path to osm map the measurement is run on for fast measurements
 # <use_measurement_time_as_ref_time> = true/false, false by default, meaning the git commit time will be used as reference
+# <spatial_rule_borders_dir> = base directory to load borders of spatial rules from
 
 # make this script exit if a command fails, a variable is missing etc.
 set -euo pipefail
@@ -17,6 +18,7 @@ defaultSingleResultsDir=measurements/results/$(date '+%d-%m-%Y-%s%N')/
 defaultSmallMap=core/files/andorra.osm.pbf
 defaultBigMap=core/files/andorra.osm.pbf
 defaultUseMeasurementTimeAsRefTime=false
+defaultBordersDirectory=core/files/spatialrules
 
 # this is the directory where we read/write data from/to
 DATA_DIR=${1:-$defaultDataDir}
@@ -26,6 +28,7 @@ SINGLE_RESULTS_DIR=${2:-$defaultSingleResultsDir}
 SMALL_OSM_MAP=${3:-$defaultSmallMap}
 BIG_OSM_MAP=${4:-$defaultBigMap}
 USE_MEASUREMENT_TIME_AS_REF_TIME=${5:-$defaultUseMeasurementTimeAsRefTime}
+BORDERS_DIRECTORY=${6:-$defaultBordersDirectory}
 
 # create directories
 mkdir -p ${DATA_DIR}
@@ -51,6 +54,7 @@ measurement.lm=false \
 graph.location=${TMP_DIR}measurement-small-gh \
 prepare.min_network_size=10000 \
 prepare.min_oneway_network_size=10000 \
+spatial_rules.borders_directory=${BORDERS_DIRECTORY} \
 measurement.json=true \
 measurement.count=5000 \
 measurement.use_measurement_time_as_ref_time=${USE_MEASUREMENT_TIME_AS_REF_TIME}
@@ -72,6 +76,7 @@ measurement.lm=true \
 graph.location=${TMP_DIR}measurement-big-gh \
 prepare.min_network_size=10000 \
 prepare.min_oneway_network_size=10000 \
+spatial_rules.borders_directory=${BORDERS_DIRECTORY} \
 measurement.json=true \
 measurement.count=5000 \
 measurement.use_measurement_time_as_ref_time=${USE_MEASUREMENT_TIME_AS_REF_TIME}
diff --git a/core/src/main/java/com/graphhopper/GraphHopper.java b/core/src/main/java/com/graphhopper/GraphHopper.java
index 02dc21905..31494d6ad 100644
--- a/core/src/main/java/com/graphhopper/GraphHopper.java
+++ b/core/src/main/java/com/graphhopper/GraphHopper.java
@@ -24,10 +24,7 @@ import com.graphhopper.json.geo.JsonFeature;
 import com.graphhopper.reader.DataReader;
 import com.graphhopper.reader.dem.*;
 import com.graphhopper.reader.osm.conditional.DateRangeParser;
-import com.graphhopper.routing.AlgorithmOptions;
-import com.graphhopper.routing.Path;
-import com.graphhopper.routing.RoutingAlgorithmFactory;
-import com.graphhopper.routing.RoutingAlgorithmFactorySimple;
+import com.graphhopper.routing.*;
 import com.graphhopper.routing.ch.CHPreparationHandler;
 import com.graphhopper.routing.ch.CHRoutingAlgorithmFactory;
 import com.graphhopper.routing.lm.LMPreparationHandler;
@@ -105,6 +102,7 @@ public class GraphHopper implements GraphHopperAPI {
     private boolean smoothElevation = false;
     // for routing
     private final RoutingConfig routingConfig = new RoutingConfig();
+    private ProfileResolver profileResolver = new ProfileResolver();
 
     // for index
     private LocationIndex locationIndex;
@@ -142,14 +140,11 @@ public class GraphHopper implements GraphHopperAPI {
         return this;
     }
 
-    /**
-     * @return the first flag encoder of the encoding manager
-     */
     FlagEncoder getDefaultVehicle() {
         if (encodingManager == null)
             throw new IllegalStateException("No encoding manager specified or loaded");
 
-        return encodingManager.fetchEdgeEncoders().get(0);
+        return profileResolver.getDefaultVehicle(encodingManager);
     }
 
     public EncodingManager getEncodingManager() {
@@ -333,6 +328,10 @@ public class GraphHopper implements GraphHopperAPI {
         return this;
     }
 
+    public List<ProfileConfig> getProfiles() {
+        return new ArrayList<>(profilesByName.values());
+    }
+
     public int getMaxVisitedNodes() {
         return routingConfig.getMaxVisitedNodes();
     }
@@ -492,6 +491,15 @@ public class GraphHopper implements GraphHopperAPI {
         return this;
     }
 
+    public ProfileResolver getProfileResolver() {
+        return this.profileResolver;
+    }
+
+    public GraphHopper setProfileResolver(ProfileResolver profileResolver) {
+        this.profileResolver = profileResolver;
+        return this;
+    }
+
     /**
      * Reads the configuration from a {@link GraphHopperConfig} object which can be manually filled, or more typically
      * is read from `config.yml`.
@@ -785,7 +793,6 @@ public class GraphHopper implements GraphHopperAPI {
 
         ghStorage.addCHGraphs(chProfiles);
 
-
         if (!new File(graphHopperFolder).exists())
             return false;
 
@@ -824,9 +831,11 @@ public class GraphHopper implements GraphHopperAPI {
                         "\nYou need to add `|turn_costs=true` to the vehicle in `graph.flag_encoders`");
             }
             try {
-                createWeighting(new HintsMap(profile.getWeighting()), encoder, NO_TURN_COST_PROVIDER);
+                createWeighting(profile, new PMap());
             } catch (IllegalArgumentException e) {
-                throw new IllegalArgumentException("The profile '" + profile.getName() + "' was configured with an unknown weighting '" + profile.getWeighting() + "'");
+                throw new IllegalArgumentException("Could not create weighting for profile: '" + profile.getName() + "'.\n" +
+                        "Profile: " + profile + "\n" +
+                        "Error: " + e.getMessage());
             }
         }
         Set<String> chProfileSet = new LinkedHashSet<>(chPreparationHandler.getCHProfileConfigs().size());
@@ -851,9 +860,14 @@ public class GraphHopper implements GraphHopperAPI {
         }
     }
 
-    public RoutingAlgorithmFactory getAlgorithmFactory(HintsMap map) {
-        boolean disableCH = map.getBool(Parameters.CH.DISABLE, false);
-        boolean disableLM = map.getBool(Parameters.Landmark.DISABLE, false);
+    public ProfileConfig resolveProfile(HintsMap hints) {
+        if (encodingManager == null)
+            throw new IllegalStateException("No encoding manager specified or loaded");
+
+        return profileResolver.resolveProfile(encodingManager, chPreparationHandler.getCHProfiles(), lmPreparationHandler.getLMProfiles(), hints);
+    }
+
+    public RoutingAlgorithmFactory getAlgorithmFactory(String profile, boolean disableCH, boolean disableLM) {
         if (chPreparationHandler.isEnabled() && disableCH && !chPreparationHandler.isDisablingAllowed()) {
             throw new IllegalArgumentException("Disabling CH is not allowed on the server side");
         }
@@ -863,9 +877,9 @@ public class GraphHopper implements GraphHopperAPI {
 
         // for now do not allow mixing CH&LM #1082,#1889
         if (chPreparationHandler.isEnabled() && !disableCH) {
-            return chPreparationHandler.getAlgorithmFactory(map);
+            return chPreparationHandler.getAlgorithmFactory(profile);
         } else if (lmPreparationHandler.isEnabled() && !disableLM) {
-            return lmPreparationHandler.getAlgorithmFactory(map);
+            return lmPreparationHandler.getAlgorithmFactory(profile);
         } else {
             return new RoutingAlgorithmFactorySimple();
         }
@@ -882,14 +896,10 @@ public class GraphHopper implements GraphHopperAPI {
 
         for (CHProfileConfig chConfig : chPreparationHandler.getCHProfileConfigs()) {
             ProfileConfig profile = profilesByName.get(chConfig.getProfile());
-            FlagEncoder encoder = encodingManager.getEncoder(profile.getVehicle());
             if (profile.isTurnCosts()) {
-                assert encoder.supportsTurnCosts() : "encoder " + encoder + " should support turn costs";
-                int uTurnCosts = profile.getHints().getInt(Routing.U_TURN_COSTS, INFINITE_U_TURN_COSTS);
-                TurnCostProvider turnCostProvider = new DefaultTurnCostProvider(encoder, ghStorage.getTurnCostStorage(), uTurnCosts);
-                chPreparationHandler.addCHProfile(CHProfile.edgeBased(profile.getName(), createWeighting(new HintsMap(profile.getWeighting()), encoder, turnCostProvider)));
+                chPreparationHandler.addCHProfile(CHProfile.edgeBased(profile.getName(), createWeighting(profile, new PMap())));
             } else {
-                chPreparationHandler.addCHProfile(CHProfile.nodeBased(profile.getName(), createWeighting(new HintsMap(profile.getWeighting()), encoder, NO_TURN_COST_PROVIDER)));
+                chPreparationHandler.addCHProfile(CHProfile.nodeBased(profile.getName(), createWeighting(profile, new PMap())));
             }
         }
     }
@@ -904,9 +914,11 @@ public class GraphHopper implements GraphHopperAPI {
 
         for (LMProfileConfig lmConfig : lmPreparationHandler.getLMProfileConfigs()) {
             ProfileConfig profile = profilesByName.get(lmConfig.getProfile());
-            FlagEncoder encoder = encodingManager.getEncoder(profile.getVehicle());
-            // note that we do not consider turn costs during LM preparation?
-            Weighting weighting = createWeighting(new HintsMap(profile.getWeighting()), encoder, NO_TURN_COST_PROVIDER);
+            // Note that turn costs will be ignored during LM preparation even when the created weighting includes
+            // turn costs, because the preparation is running node-based. This is important if we want to allow e.g.
+            // changing the u_turn_costs per request (we have to use the minimum weight settings (= no turn costs) for
+            // the preparation)
+            Weighting weighting = createWeighting(profile, new PMap());
             lmPreparationHandler.addLMProfile(new LMProfile(profile.getName(), weighting));
         }
     }
@@ -983,18 +995,11 @@ public class GraphHopper implements GraphHopperAPI {
     }
 
     /**
-     * Based on the hintsMap and the specified encoder a Weighting instance can be
-     * created. Note that all URL parameters are available in the hintsMap as String if
-     * you use the web module.
-     *
-     * @param hintsMap all parameters influencing the weighting. E.g. parameters coming via
-     *                 GHRequest.getHints or directly via "&amp;api.xy=" from the URL of the web UI
-     * @param encoder  the required vehicle
-     * @return the weighting to be used for route calculation
-     * @see HintsMap
+     * @param profileConfig The profile for which the weighting shall be created
+     * @param hints         Additional hints that can be used to further specify the weighting that shall be created
      */
-    public Weighting createWeighting(HintsMap hintsMap, FlagEncoder encoder, TurnCostProvider turnCostProvider) {
-        return new DefaultWeightingFactory().createWeighting(hintsMap, encoder, turnCostProvider);
+    public Weighting createWeighting(ProfileConfig profileConfig, PMap hints) {
+        return new DefaultWeightingFactory(encodingManager, ghStorage).createWeighting(profileConfig, hints);
     }
 
     @Override
@@ -1017,36 +1022,10 @@ public class GraphHopper implements GraphHopperAPI {
         if (locationIndex == null)
             throw new IllegalStateException("Location index not initialized");
 
-        // default handling
-        String vehicle = request.getVehicle();
-        if (vehicle.isEmpty()) {
-            vehicle = getDefaultVehicle().toString();
-            request.setVehicle(vehicle);
-        }
-
         Lock readLock = readWriteLock.readLock();
         readLock.lock();
         try {
-            if (!encodingManager.hasEncoder(vehicle))
-                throw new IllegalArgumentException("Vehicle not supported: " + vehicle + ". Supported are: " + encodingManager.toString());
-
-            FlagEncoder encoder = encodingManager.getEncoder(vehicle);
             HintsMap hints = request.getHints();
-
-            // we use edge-based routing if the encoder supports turn-costs *unless* the edge_based parameter is set
-            // explicitly.
-            TraversalMode tMode = encoder.supportsTurnCosts() ? TraversalMode.EDGE_BASED : TraversalMode.NODE_BASED;
-            if (hints.has(Routing.EDGE_BASED))
-                tMode = hints.getBool(Routing.EDGE_BASED, false) ? TraversalMode.EDGE_BASED : TraversalMode.NODE_BASED;
-
-            if (tMode.isEdgeBased() && !encoder.supportsTurnCosts()) {
-                throw new IllegalArgumentException("You need to set up a turn cost storage to make use of edge_based=true, e.g. use car|turn_costs=true");
-            }
-
-            if (!tMode.isEdgeBased() && !request.getCurbsides().isEmpty()) {
-                throw new IllegalArgumentException("To make use of the " + CURBSIDE + " parameter you need to set " + Routing.EDGE_BASED + " to true");
-            }
-
             boolean disableCH = hints.getBool(CH.DISABLE, false);
             if (chPreparationHandler.isEnabled() && !chPreparationHandler.isDisablingAllowed() && disableCH)
                 throw new IllegalArgumentException("Disabling CH not allowed on the server-side");
@@ -1059,7 +1038,7 @@ public class GraphHopper implements GraphHopperAPI {
                 if (request.hasFavoredHeading(0))
                     throw new IllegalArgumentException("The 'heading' parameter is currently not supported for speed mode, you need to disable speed mode with `ch.disable=true`. See issue #483");
 
-                if (request.getHints().getBool(Routing.PASS_THROUGH, false))
+                if (hints.getBool(Routing.PASS_THROUGH, false))
                     throw new IllegalArgumentException("The '" + Parameters.Routing.PASS_THROUGH + "' parameter is currently not supported for speed mode, you need to disable speed mode with `ch.disable=true`. See issue #1765");
             }
 
@@ -1072,7 +1051,13 @@ public class GraphHopper implements GraphHopperAPI {
             // For example see #734
             checkIfPointsAreInBounds(points);
 
-            RoutingAlgorithmFactory algorithmFactory = getAlgorithmFactory(hints);
+            ProfileConfig profile = resolveProfile(hints);
+            if (!profile.isTurnCosts() && !request.getCurbsides().isEmpty())
+                throw new IllegalArgumentException("To make use of the " + CURBSIDE + " parameter you need to use a profile that supports turn costs");
+
+            TraversalMode tMode = profile.isTurnCosts() ? TraversalMode.EDGE_BASED : TraversalMode.NODE_BASED;
+
+            RoutingAlgorithmFactory algorithmFactory = getAlgorithmFactory(profile.getName(), disableCH, disableLM);
             Weighting weighting;
             Graph graph = ghStorage;
             if (chPreparationHandler.isEnabled() && !disableCH) {
@@ -1088,14 +1073,12 @@ public class GraphHopper implements GraphHopperAPI {
                 }
             } else {
                 checkNonChMaxWaypointDistance(points);
-                final int uTurnCostsInt = request.getHints().getInt(Routing.U_TURN_COSTS, INFINITE_U_TURN_COSTS);
+                final int uTurnCostsInt = hints.getInt(Routing.U_TURN_COSTS, INFINITE_U_TURN_COSTS);
                 if (uTurnCostsInt != INFINITE_U_TURN_COSTS && !tMode.isEdgeBased()) {
                     throw new IllegalArgumentException("Finite u-turn costs can only be used for edge-based routing, use `" + Routing.EDGE_BASED + "=true'");
                 }
-                TurnCostProvider turnCostProvider = (encoder.supportsTurnCosts() && tMode.isEdgeBased())
-                        ? new DefaultTurnCostProvider(encoder, ghStorage.getTurnCostStorage(), uTurnCostsInt)
-                        : NO_TURN_COST_PROVIDER;
-                weighting = createWeighting(hints, encoder, turnCostProvider);
+                FlagEncoder encoder = encodingManager.getEncoder(profile.getVehicle());
+                weighting = createWeighting(profile, hints);
                 if (hints.has(Routing.BLOCK_AREA))
                     weighting = new BlockAreaWeighting(weighting, GraphEdgeIdFinder.createBlockArea(ghStorage, locationIndex,
                             points, hints, DefaultEdgeFilter.allEdges(encoder)));
@@ -1117,7 +1100,9 @@ public class GraphHopper implements GraphHopperAPI {
                 throw new IllegalArgumentException("The max_visited_nodes parameter has to be below or equal to:" + routingConfig.getMaxVisitedNodes());
 
             AlgorithmOptions algoOpts = AlgorithmOptions.start().
-                    algorithm(algoStr).traversalMode(tMode).weighting(weighting).
+                    algorithm(algoStr).
+                    traversalMode(tMode).
+                    weighting(weighting).
                     maxVisitedNodes(maxVisitedNodesForRequest).
                     hints(hints).
                     build();
@@ -1352,27 +1337,49 @@ public class GraphHopper implements GraphHopperAPI {
     }
 
     private static class DefaultWeightingFactory {
-        public Weighting createWeighting(HintsMap hintsMap, FlagEncoder encoder, TurnCostProvider turnCostProvider) {
-            String weightingStr = toLowerCase(hintsMap.getWeighting());
+        private final EncodingManager encodingManager;
+        private final GraphHopperStorage ghStorage;
+
+        public DefaultWeightingFactory(EncodingManager encodingManager, GraphHopperStorage ghStorage) {
+            this.encodingManager = encodingManager;
+            this.ghStorage = ghStorage;
+        }
+
+        public Weighting createWeighting(ProfileConfig profile, PMap hints) {
+            FlagEncoder encoder = encodingManager.getEncoder(profile.getVehicle());
+            TurnCostProvider turnCostProvider;
+            if (profile.isTurnCosts()) {
+                if (!encoder.supportsTurnCosts())
+                    throw new IllegalArgumentException("Encoder " + encoder + " does not support turn costs");
+                int uTurnCosts = profile.getHints().getInt(Routing.U_TURN_COSTS, INFINITE_U_TURN_COSTS);
+                turnCostProvider = new DefaultTurnCostProvider(encoder, ghStorage.getTurnCostStorage(), uTurnCosts);
+            } else {
+                turnCostProvider = NO_TURN_COST_PROVIDER;
+            }
+
+            String weightingStr = toLowerCase(profile.getWeighting());
+            if (weightingStr.isEmpty())
+                throw new IllegalArgumentException("You have to specify a weighting");
+
             Weighting weighting = null;
 
             if ("shortest".equalsIgnoreCase(weightingStr)) {
                 weighting = new ShortestWeighting(encoder, turnCostProvider);
-            } else if ("fastest".equalsIgnoreCase(weightingStr) || weightingStr.isEmpty()) {
+            } else if ("fastest".equalsIgnoreCase(weightingStr)) {
                 if (encoder.supports(PriorityWeighting.class))
-                    weighting = new PriorityWeighting(encoder, hintsMap, turnCostProvider);
+                    weighting = new PriorityWeighting(encoder, hints, turnCostProvider);
                 else
-                    weighting = new FastestWeighting(encoder, hintsMap, turnCostProvider);
+                    weighting = new FastestWeighting(encoder, hints, turnCostProvider);
             } else if ("curvature".equalsIgnoreCase(weightingStr)) {
                 if (encoder.supports(CurvatureWeighting.class))
-                    weighting = new CurvatureWeighting(encoder, hintsMap, turnCostProvider);
+                    weighting = new CurvatureWeighting(encoder, hints, turnCostProvider);
 
             } else if ("short_fastest".equalsIgnoreCase(weightingStr)) {
-                weighting = new ShortFastestWeighting(encoder, hintsMap, turnCostProvider);
+                weighting = new ShortFastestWeighting(encoder, hints, turnCostProvider);
             }
 
             if (weighting == null)
-                throw new IllegalArgumentException("weighting " + weightingStr + " not supported");
+                throw new IllegalArgumentException("Weighting '" + weightingStr + "' not supported");
 
             return weighting;
         }
diff --git a/core/src/main/java/com/graphhopper/routing/ProfileResolver.java b/core/src/main/java/com/graphhopper/routing/ProfileResolver.java
new file mode 100644
index 000000000..b503b774f
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/ProfileResolver.java
@@ -0,0 +1,209 @@
+/*
+ *  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;
+
+import com.graphhopper.config.ProfileConfig;
+import com.graphhopper.routing.lm.LMProfile;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.routing.util.FlagEncoder;
+import com.graphhopper.routing.util.HintsMap;
+import com.graphhopper.storage.CHProfile;
+import com.graphhopper.util.Parameters;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.graphhopper.routing.weighting.Weighting.INFINITE_U_TURN_COSTS;
+
+public class ProfileResolver {
+
+    public ProfileConfig resolveProfile(EncodingManager encodingManager, List<CHProfile> chProfiles, List<LMProfile> lmProfiles, HintsMap hints) {
+        // default handling
+        String vehicle = hints.getVehicle();
+        if (vehicle.isEmpty()) {
+            vehicle = getDefaultVehicle(encodingManager).toString();
+        }
+        String weighting = hints.getWeighting();
+        if (weighting.isEmpty()) {
+            weighting = "fastest";
+        }
+        if (!encodingManager.hasEncoder(vehicle))
+            throw new IllegalArgumentException("Vehicle not supported: " + vehicle + ". Supported are: " + encodingManager.toString());
+
+        FlagEncoder encoder = encodingManager.getEncoder(vehicle);
+        // we use turn costs if the encoder supports it *unless* the edge_based parameter is set explicitly
+        boolean turnCosts = encoder.supportsTurnCosts();
+        if (hints.has(Parameters.Routing.EDGE_BASED))
+            turnCosts = hints.getBool(Parameters.Routing.EDGE_BASED, false);
+        if (turnCosts && !encoder.supportsTurnCosts())
+            throw new IllegalArgumentException("You need to set up a turn cost storage to make use of edge_based=true, e.g. use car|turn_costs=true");
+
+        String profileName = resolveProfileName(chProfiles, lmProfiles, hints);
+
+        return new ProfileConfig(profileName)
+                .setVehicle(vehicle)
+                .setWeighting(weighting)
+                .setTurnCosts(turnCosts);
+    }
+
+    private String resolveProfileName(List<CHProfile> chProfiles, List<LMProfile> lmProfiles, HintsMap hints) {
+        boolean disableCH = hints.getBool(Parameters.CH.DISABLE, false);
+        boolean disableLM = hints.getBool(Parameters.Landmark.DISABLE, false);
+
+        String profileName;
+        if (!chProfiles.isEmpty() && !disableCH) {
+            profileName = selectCHProfile(chProfiles, hints).getName();
+        } else if (!lmProfiles.isEmpty() && !disableLM) {
+            profileName = selectLMProfile(lmProfiles, hints).getName();
+        } else {
+            // todonow: here we will instead select one of the existing profiles
+            profileName = "unprepared_profile";
+        }
+        return profileName;
+    }
+
+    /**
+     * @param chProfiles the CH profiles to choose from
+     * @param hintsMap   a map used to describe the CH profile that shall be selected
+     * @throws IllegalArgumentException if no CH profile could be selected for the given parameters
+     */
+    public CHProfile selectCHProfile(List<CHProfile> chProfiles, HintsMap hintsMap) {
+        int numMatchingEdgeBased = 0;
+        List<CHProfile> matchingProfiles = new ArrayList<>();
+        for (CHProfile p : chProfiles) {
+            if (!chProfileMatchesHints(p, hintsMap))
+                continue;
+            matchingProfiles.add(p);
+            if (p.isEdgeBased()) {
+                numMatchingEdgeBased++;
+            }
+        }
+
+        Boolean edgeBased = getEdgeBased(hintsMap);
+        Integer uTurnCosts = getUTurnCosts(hintsMap);
+        if (matchingProfiles.isEmpty()) {
+            throw new IllegalArgumentException("Cannot find matching CH profile for your request. Please check your parameters." +
+                    "\nYou can try disabling CH using " + Parameters.CH.DISABLE + "=true" +
+                    "\nrequested:  " + getCHRequestAsString(hintsMap, edgeBased, uTurnCosts) + "\navailable: " + chProfiles);
+        } else if (matchingProfiles.size() == 1) {
+            return matchingProfiles.get(0);
+        } else {
+            // special case: prefer edge-based over node-based if these are the only two options
+            CHProfile match1 = matchingProfiles.get(0);
+            CHProfile match2 = matchingProfiles.get(1);
+            if (edgeBased == null && matchingProfiles.size() == 2 &&
+                    match1.getWeighting().getName().equals(match2.getWeighting().getName()) &&
+                    match1.getWeighting().getFlagEncoder().toString().equals(match2.getWeighting().getFlagEncoder().toString()) &&
+                    match1.isEdgeBased() != match2.isEdgeBased()) {
+                return match1.isEdgeBased() ? match1 : match2;
+            }
+            // special case: error if multiple edge-based matches. to differentiate between these it will be required
+            // to explicitly set the profile parameter.
+            if (numMatchingEdgeBased > 1 && numMatchingEdgeBased == matchingProfiles.size()) {
+                throw new IllegalArgumentException("There are multiple edge-based CH profiles matching your request. You need to" +
+                        " specify the profile you want to use explicitly, see here: https://github.com/graphhopper/graphhopper/pull/1934.");
+            } else {
+                throw new IllegalArgumentException("There are multiple CH profiles matching your request. Use the `weighting`,`vehicle`,`edge_based` and/or `u_turn_costs` parameters to be more specific." +
+                        "\nYou can also try disabling CH altogether using " + Parameters.CH.DISABLE + "=true" +
+                        "\nrequested:  " + getCHRequestAsString(hintsMap, edgeBased, uTurnCosts) + "\nmatched:   " + matchingProfiles + "\navailable: " + chProfiles);
+
+            }
+        }
+    }
+
+    public LMProfile selectLMProfile(List<LMProfile> lmProfiles, HintsMap hintsMap) {
+        List<LMProfile> matchingProfiles = new ArrayList<>();
+        for (LMProfile p : lmProfiles) {
+            if (!lmProfileMatchesHints(p, hintsMap))
+                continue;
+            matchingProfiles.add(p);
+        }
+        // Note:
+        // There are situations where we can use the requested encoder/weighting with an existing LM preparation, even
+        // though the preparation was done with a different weighting. For example this works when the new weighting
+        // only yields higher (but never lower) weights than the one that was used for the preparation. However, its not
+        // trivial to check whether or not this is the case so we do not allow this for now.
+        if (matchingProfiles.isEmpty()) {
+            throw new IllegalArgumentException("Cannot find matching LM profile for your request. Please check your parameters." +
+                    "\nYou can try disabling LM by setting " + Parameters.Landmark.DISABLE + "=true" +
+                    "\nrequested: " + getLMRequestAsString(hintsMap) + "\navailable: " + lmProfilesAsStrings(lmProfiles));
+        } else if (matchingProfiles.size() == 1) {
+            return matchingProfiles.get(0);
+        } else {
+            throw new IllegalArgumentException("There are multiple LM profiles matching your request. Use the `weighting` and `vehicle` parameters to be more specific." +
+                    "\nYou can also try disabling LM altogether using " + Parameters.CH.DISABLE + "=true" +
+                    "\nrequested:  " + getLMRequestAsString(hintsMap) + "\nmatched:   " + lmProfilesAsStrings(matchingProfiles) + "\navailable: " + lmProfilesAsStrings(lmProfiles));
+        }
+    }
+
+    protected boolean chProfileMatchesHints(CHProfile p, HintsMap hintsMap) {
+        Boolean edgeBased = getEdgeBased(hintsMap);
+        Integer uTurnCosts = getUTurnCosts(hintsMap);
+        return (edgeBased == null || p.isEdgeBased() == edgeBased) &&
+                // u-turn costs cannot be used to select one of multiple edge-based CH profiles,
+                // but when they are set only edge-based profiles can match
+                (uTurnCosts == null || p.isEdgeBased()) &&
+                (hintsMap.getWeighting().isEmpty() || p.getWeighting().getName().equals(hintsMap.getWeighting())) &&
+                (hintsMap.getVehicle().isEmpty() || p.getWeighting().getFlagEncoder().toString().equals(hintsMap.getVehicle()));
+    }
+
+    protected boolean lmProfileMatchesHints(LMProfile p, HintsMap hintsMap) {
+        return (hintsMap.getWeighting().isEmpty() || p.getWeighting().getName().equals(hintsMap.getWeighting())) &&
+                (hintsMap.getVehicle().isEmpty() || p.getWeighting().getFlagEncoder().toString().equals(hintsMap.getVehicle()));
+    }
+
+    /**
+     * @return the first flag encoder of the encoding manager
+     */
+    public FlagEncoder getDefaultVehicle(EncodingManager encodingManager) {
+        return encodingManager.fetchEdgeEncoders().get(0);
+    }
+
+    private String getLMRequestAsString(HintsMap map) {
+        return (map.getWeighting().isEmpty() ? "*" : map.getWeighting()) +
+                "|" +
+                (map.getVehicle().isEmpty() ? "*" : map.getVehicle());
+    }
+
+    private String getCHRequestAsString(HintsMap hintsMap, Boolean edgeBased, Integer uTurnCosts) {
+        return (hintsMap.getWeighting().isEmpty() ? "*" : hintsMap.getWeighting()) +
+                "|" +
+                (hintsMap.getVehicle().isEmpty() ? "*" : hintsMap.getVehicle()) +
+                "|" +
+                "edge_based=" + (edgeBased != null ? edgeBased : "*") +
+                "|" +
+                "u_turn_costs=" + (uTurnCosts != null ? uTurnCosts : "*");
+    }
+
+    private List<String> lmProfilesAsStrings(List<LMProfile> profiles) {
+        List<String> result = new ArrayList<>(profiles.size());
+        for (LMProfile p : profiles) {
+            result.add(p.getWeighting().getName() + "|" + p.getWeighting().getFlagEncoder().toString());
+        }
+        return result;
+    }
+
+    private Boolean getEdgeBased(HintsMap hintsMap) {
+        return hintsMap.has(Parameters.Routing.EDGE_BASED) ? hintsMap.getBool(Parameters.Routing.EDGE_BASED, false) : null;
+    }
+
+    private Integer getUTurnCosts(HintsMap hintsMap) {
+        return hintsMap.has(Parameters.Routing.U_TURN_COSTS) ? hintsMap.getInt(Parameters.Routing.U_TURN_COSTS, INFINITE_U_TURN_COSTS) : null;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/ch/CHPreparationHandler.java b/core/src/main/java/com/graphhopper/routing/ch/CHPreparationHandler.java
index fc13f0300..03d3da7c4 100644
--- a/core/src/main/java/com/graphhopper/routing/ch/CHPreparationHandler.java
+++ b/core/src/main/java/com/graphhopper/routing/ch/CHPreparationHandler.java
@@ -20,7 +20,6 @@ package com.graphhopper.routing.ch;
 import com.graphhopper.GraphHopperConfig;
 import com.graphhopper.config.CHProfileConfig;
 import com.graphhopper.routing.RoutingAlgorithmFactory;
-import com.graphhopper.routing.util.HintsMap;
 import com.graphhopper.storage.CHProfile;
 import com.graphhopper.storage.GraphHopperStorage;
 import com.graphhopper.storage.StorableProperties;
@@ -164,34 +163,29 @@ public class CHPreparationHandler {
 
     /**
      * @return a {@link RoutingAlgorithmFactory} for CH or throw an error if no preparation is available for the given
-     * hints
+     * profile name
      */
-    public RoutingAlgorithmFactory getAlgorithmFactory(HintsMap map) {
-        if (preparations.isEmpty())
-            throw new IllegalStateException("No CH preparations added yet");
-        return getPreparation(map).getRoutingAlgorithmFactory();
+    public RoutingAlgorithmFactory getAlgorithmFactory(String profile) {
+        return getPreparation(profile).getRoutingAlgorithmFactory();
     }
 
-    public PrepareContractionHierarchies getPreparation(HintsMap map) {
-        CHProfile selectedProfile = selectProfile(map);
-        return getPreparation(selectedProfile);
-    }
-
-    public PrepareContractionHierarchies getPreparation(CHProfile chProfile) {
-        for (PrepareContractionHierarchies p : getPreparations()) {
-            if (p.getCHProfile().equals(chProfile)) {
-                return p;
+    public PrepareContractionHierarchies getPreparation(String profile) {
+        if (preparations.isEmpty())
+            throw new IllegalStateException("No CH preparations added yet");
+        List<String> profileNames = new ArrayList<>(preparations.size());
+        for (PrepareContractionHierarchies preparation : preparations) {
+            profileNames.add(preparation.getCHProfile().getName());
+            if (preparation.getCHProfile().getName().equalsIgnoreCase(profile)) {
+                return preparation;
             }
         }
-        throw new IllegalStateException("Could not find CH preparation for profile: " + chProfile);
+        throw new IllegalArgumentException("Cannot find CH preparation for the requested profile: '" + profile + "'" +
+                "\nYou can try disabling CH using " + CH.DISABLE + "=true" +
+                "\navailable CH profiles: " + profileNames);
     }
 
-    private CHProfile selectProfile(HintsMap map) {
-        try {
-            return CHProfileSelector.select(chProfiles, map);
-        } catch (CHProfileSelectionException e) {
-            throw new IllegalArgumentException(e.getMessage());
-        }
+    public PrepareContractionHierarchies getPreparation(CHProfile chProfile) {
+        return getPreparation(chProfile.getName());
     }
 
     public int getPreparationThreads() {
diff --git a/core/src/main/java/com/graphhopper/routing/ch/CHProfileSelectionException.java b/core/src/main/java/com/graphhopper/routing/ch/CHProfileSelectionException.java
deleted file mode 100644
index 531b5bdb5..000000000
--- a/core/src/main/java/com/graphhopper/routing/ch/CHProfileSelectionException.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- *  Licensed to GraphHopper GmbH under one or more contributor
- *  license agreements. See the NOTICE file distributed with this work for
- *  additional information regarding copyright ownership.
- *
- *  GraphHopper GmbH licenses this file to you under the Apache License,
- *  Version 2.0 (the "License"); you may not use this file except in
- *  compliance with the License. You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.graphhopper.routing.ch;
-
-import com.graphhopper.util.exceptions.DetailedIllegalArgumentException;
-
-import java.util.HashMap;
-
-public class CHProfileSelectionException extends DetailedIllegalArgumentException {
-    CHProfileSelectionException(final String message) {
-        super(message, new HashMap<String, Object>() {{
-            put("error", message);
-        }});
-    }
-}
diff --git a/core/src/main/java/com/graphhopper/routing/ch/CHProfileSelector.java b/core/src/main/java/com/graphhopper/routing/ch/CHProfileSelector.java
deleted file mode 100644
index 24ad6809c..000000000
--- a/core/src/main/java/com/graphhopper/routing/ch/CHProfileSelector.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- *  Licensed to GraphHopper GmbH under one or more contributor
- *  license agreements. See the NOTICE file distributed with this work for
- *  additional information regarding copyright ownership.
- *
- *  GraphHopper GmbH licenses this file to you under the Apache License,
- *  Version 2.0 (the "License"); you may not use this file except in
- *  compliance with the License. You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.graphhopper.routing.ch;
-
-import com.graphhopper.routing.util.HintsMap;
-import com.graphhopper.storage.CHProfile;
-import com.graphhopper.util.Parameters;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static com.graphhopper.routing.weighting.Weighting.INFINITE_U_TURN_COSTS;
-
-/**
- * This class is used to determine the appropriate CH profile given (or not given) some (request) parameters
- */
-public class CHProfileSelector {
-    private final List<CHProfile> chProfiles;
-    private final HintsMap hintsMap;
-    private final Boolean edgeBased;
-    private final Integer uTurnCosts;
-
-    private CHProfileSelector(List<CHProfile> chProfiles, HintsMap hintsMap) {
-        this.chProfiles = chProfiles;
-        this.hintsMap = hintsMap;
-        edgeBased = hintsMap.has(Parameters.Routing.EDGE_BASED) ? hintsMap.getBool(Parameters.Routing.EDGE_BASED, false) : null;
-        uTurnCosts = hintsMap.has(Parameters.Routing.U_TURN_COSTS) ? hintsMap.getInt(Parameters.Routing.U_TURN_COSTS, INFINITE_U_TURN_COSTS) : null;
-    }
-
-    /**
-     * @param chProfiles the CH profiles to choose from
-     * @param hintsMap   a map used to describe the CH profile that shall be selected
-     * @throws CHProfileSelectionException if no CH profile could be selected for the given parameters
-     */
-    public static CHProfile select(List<CHProfile> chProfiles, HintsMap hintsMap) {
-        return new CHProfileSelector(chProfiles, hintsMap).select();
-    }
-
-    private CHProfile select() {
-
-        List<CHProfile> matchingProfiles = new ArrayList<>();
-        for (CHProfile p : chProfiles) {
-            if (edgeBased != null && p.isEdgeBased() != edgeBased) {
-                continue;
-            }
-            if (!p.getWeighting().matches(hintsMap)) {
-                continue;
-            }
-            matchingProfiles.add(p);
-        }
-
-        if (matchingProfiles.isEmpty()) {
-            throw new CHProfileSelectionException("Cannot find matching CH profile for your request. Please check your parameters." +
-                    "\nYou can try disabling CH using " + Parameters.CH.DISABLE + "=true" +
-                    "\nrequested:  " + getRequestAsString() + "\navailable: " + chProfiles);
-        } else if (matchingProfiles.size() == 1) {
-            return matchingProfiles.get(0);
-        } else {
-            // special case: prefer edge-based over node-based if these are the only two options
-            CHProfile match1 = matchingProfiles.get(0);
-            CHProfile match2 = matchingProfiles.get(1);
-            if (edgeBased == null && matchingProfiles.size() == 2 &&
-                    getWeighting(match1).equals(getWeighting(match2)) &&
-                    getVehicle(match1).equals(getVehicle(match2)) &&
-                    match1.isEdgeBased() != match2.isEdgeBased()) {
-                return match1.isEdgeBased() ? match1 : match2;
-            }
-            throw new CHProfileSelectionException("There are multiple CH profiles matching your request. Use the `weighting`,`vehicle`,`edge_based` and/or `u_turn_costs` parameters to be more specific." +
-                    "\nYou can also try disabling CH altogether using " + Parameters.CH.DISABLE + "=true" +
-                    "\nrequested:  " + getRequestAsString() + "\nmatched:   " + matchingProfiles + "\navailable: " + chProfiles);
-        }
-    }
-
-    private String getVehicle(CHProfile match1) {
-        return match1.getWeighting().getFlagEncoder().toString();
-    }
-
-    private String getWeighting(CHProfile match1) {
-        return match1.getWeighting().getName();
-    }
-
-    private String getRequestAsString() {
-        String sb = (hintsMap.getWeighting().isEmpty() ? "*" : hintsMap.getWeighting()) +
-                "|" +
-                (hintsMap.getVehicle().isEmpty() ? "*" : hintsMap.getVehicle()) +
-                "|" +
-                "edge_based=" + (edgeBased != null ? edgeBased : "*") +
-                "|" +
-                "u_turn_costs=" + (uTurnCosts != null ? uTurnCosts : "*");
-        return sb;
-    }
-}
diff --git a/core/src/main/java/com/graphhopper/routing/lm/LMPreparationHandler.java b/core/src/main/java/com/graphhopper/routing/lm/LMPreparationHandler.java
index 5669f8a3c..b838c0d31 100644
--- a/core/src/main/java/com/graphhopper/routing/lm/LMPreparationHandler.java
+++ b/core/src/main/java/com/graphhopper/routing/lm/LMPreparationHandler.java
@@ -21,7 +21,6 @@ import com.graphhopper.GraphHopperConfig;
 import com.graphhopper.config.LMProfileConfig;
 import com.graphhopper.routing.RoutingAlgorithmFactory;
 import com.graphhopper.routing.ch.CHPreparationHandler;
-import com.graphhopper.routing.util.HintsMap;
 import com.graphhopper.storage.GraphHopperStorage;
 import com.graphhopper.storage.StorableProperties;
 import com.graphhopper.storage.index.LocationIndex;
@@ -181,33 +180,27 @@ public class LMPreparationHandler {
 
     /**
      * @return a {@link RoutingAlgorithmFactory} for LM or throw an error if no preparation is available for the given
-     * hints
+     * profile name
      */
-    public RoutingAlgorithmFactory getAlgorithmFactory(HintsMap map) {
-        PrepareLandmarks preparation = getPreparation(map);
+    public RoutingAlgorithmFactory getAlgorithmFactory(String profile) {
+        PrepareLandmarks preparation = getPreparation(profile);
         return preparation.getRoutingAlgorithmFactory().setDefaultActiveLandmarks(activeLandmarkCount);
     }
 
-    private PrepareLandmarks getPreparation(HintsMap map) {
+    private PrepareLandmarks getPreparation(String profile) {
         if (preparations.isEmpty())
             throw new IllegalStateException("No LM preparations added yet");
 
-        List<String> lmProfiles = new ArrayList<>(preparations.size());
-        for (final PrepareLandmarks p : preparations) {
-            lmProfiles.add(p.getLMProfile().getName());
-            if (p.getLMProfile().getWeighting().matches(map))
-                return p;
+        List<String> profileNames = new ArrayList<>(preparations.size());
+        for (PrepareLandmarks preparation : preparations) {
+            profileNames.add(preparation.getLMProfile().getName());
+            if (preparation.getLMProfile().getName().equals(profile)) {
+                return preparation;
+            }
         }
-
-        // There are situations where we can use the requested encoder/weighting with an existing LM preparation, even
-        // though the preparation was done with a different weighting. For example this works when the new weighting
-        // only yields higher (but never lower) weights than the one that was used for the preparation. However, its not
-        // trivial to check whether or not this is the case so we do not allow this for now.
-        String requestedString = (map.getWeighting().isEmpty() ? "*" : map.getWeighting()) + "|" +
-                (map.getVehicle().isEmpty() ? "*" : map.getVehicle());
-        throw new IllegalArgumentException("Cannot find matching LM profile for your request. Please check your parameters." +
-                "\nYou can try disabling LM by setting " + Parameters.Landmark.DISABLE + "=true" +
-                "\nrequested: " + requestedString + "\navailable: " + lmProfiles);
+        throw new IllegalArgumentException("Cannot find LM preparation for the requested profile: '" + profile + "'" +
+                "\nYou can try disabling LM using " + Parameters.Landmark.DISABLE + "=true" +
+                "\navailable LM profiles: " + profileNames);
     }
 
     /**
diff --git a/core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java b/core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java
index f3a822559..76c77ddb1 100644
--- a/core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java
+++ b/core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java
@@ -118,6 +118,7 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
 
         // Edge based is not really necessary because when adding turn costs while routing we can still
         // use the node based traversal as this is a smaller weight approximation and will still produce correct results
+        // In this sense its even 'better' to use node-based.
         this.traversalMode = TraversalMode.NODE_BASED;
         this.landmarkWeightDA = dir.find("landmarks_" + lmProfile.getName());
 
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/AbstractAdjustedWeighting.java b/core/src/main/java/com/graphhopper/routing/weighting/AbstractAdjustedWeighting.java
index 7b794cee6..00f9893d5 100644
--- a/core/src/main/java/com/graphhopper/routing/weighting/AbstractAdjustedWeighting.java
+++ b/core/src/main/java/com/graphhopper/routing/weighting/AbstractAdjustedWeighting.java
@@ -18,7 +18,6 @@
 package com.graphhopper.routing.weighting;
 
 import com.graphhopper.routing.util.FlagEncoder;
-import com.graphhopper.routing.util.HintsMap;
 import com.graphhopper.util.EdgeIteratorState;
 
 /**
@@ -68,12 +67,6 @@ public abstract class AbstractAdjustedWeighting implements Weighting {
         return superWeighting.getFlagEncoder();
     }
 
-    @Override
-    public boolean matches(HintsMap reqMap) {
-        return getName().equals(reqMap.getWeighting())
-                && superWeighting.getFlagEncoder().toString().equals(reqMap.getVehicle());
-    }
-
     @Override
     public String toString() {
         return getName() + "|" + superWeighting.toString();
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/AbstractWeighting.java b/core/src/main/java/com/graphhopper/routing/weighting/AbstractWeighting.java
index 573176d5b..363b40f26 100644
--- a/core/src/main/java/com/graphhopper/routing/weighting/AbstractWeighting.java
+++ b/core/src/main/java/com/graphhopper/routing/weighting/AbstractWeighting.java
@@ -20,9 +20,7 @@ package com.graphhopper.routing.weighting;
 import com.graphhopper.routing.profiles.BooleanEncodedValue;
 import com.graphhopper.routing.profiles.DecimalEncodedValue;
 import com.graphhopper.routing.util.FlagEncoder;
-import com.graphhopper.routing.util.HintsMap;
 import com.graphhopper.util.EdgeIteratorState;
-import com.graphhopper.util.Parameters;
 
 import static com.graphhopper.routing.weighting.TurnCostProvider.NO_TURN_COST_PROVIDER;
 import static com.graphhopper.util.Helper.isEmpty;
@@ -92,14 +90,6 @@ public abstract class AbstractWeighting implements Weighting {
         return turnCostProvider.calcTurnMillis(inEdge, viaNode, outEdge);
     }
 
-    @Override
-    public boolean matches(HintsMap reqMap) {
-        String requestedUTurnCosts = reqMap.get(Parameters.Routing.U_TURN_COSTS, "");
-        return (reqMap.getWeighting().isEmpty() || getName().equals(reqMap.getWeighting())) &&
-                (requestedUTurnCosts.isEmpty() || turnCostProvider.getName().equals(requestedUTurnCosts)) &&
-                (reqMap.getVehicle().isEmpty() || flagEncoder.toString().equals(reqMap.getVehicle()));
-    }
-
     @Override
     public FlagEncoder getFlagEncoder() {
         return flagEncoder;
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/QueryGraphWeighting.java b/core/src/main/java/com/graphhopper/routing/weighting/QueryGraphWeighting.java
index aaf161d4b..134620961 100644
--- a/core/src/main/java/com/graphhopper/routing/weighting/QueryGraphWeighting.java
+++ b/core/src/main/java/com/graphhopper/routing/weighting/QueryGraphWeighting.java
@@ -21,7 +21,6 @@ package com.graphhopper.routing.weighting;
 import com.carrotsearch.hppc.IntArrayList;
 import com.graphhopper.routing.querygraph.QueryGraph;
 import com.graphhopper.routing.util.FlagEncoder;
-import com.graphhopper.routing.util.HintsMap;
 import com.graphhopper.util.EdgeIterator;
 import com.graphhopper.util.EdgeIteratorState;
 
@@ -105,11 +104,6 @@ public class QueryGraphWeighting implements Weighting {
         return weighting.getName();
     }
 
-    @Override
-    public boolean matches(HintsMap map) {
-        return weighting.matches(map);
-    }
-
     private int getOriginalEdge(int edge) {
         return closestEdges.get((edge - firstVirtualEdgeId) / 4);
     }
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/Weighting.java b/core/src/main/java/com/graphhopper/routing/weighting/Weighting.java
index cd84eb8fc..07e452fc1 100644
--- a/core/src/main/java/com/graphhopper/routing/weighting/Weighting.java
+++ b/core/src/main/java/com/graphhopper/routing/weighting/Weighting.java
@@ -18,7 +18,6 @@
 package com.graphhopper.routing.weighting;
 
 import com.graphhopper.routing.util.FlagEncoder;
-import com.graphhopper.routing.util.HintsMap;
 import com.graphhopper.util.EdgeIteratorState;
 
 /**
@@ -65,8 +64,4 @@ public interface Weighting {
 
     String getName();
 
-    /**
-     * Returns true if the specified weighting and encoder matches to this Weighting.
-     */
-    boolean matches(HintsMap map);
 }
diff --git a/tools/pom.xml b/tools/pom.xml
index 3e69cc52f..aa73df5be 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -3,9 +3,7 @@
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
-    <groupId>com.graphhopper</groupId>
     <artifactId>graphhopper-tools</artifactId>
-    <version>1.0-SNAPSHOT</version>
     <packaging>jar</packaging>
     <name>GraphHopper Tools</name>
 
@@ -38,6 +36,10 @@
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.graphhopper.external</groupId>
+            <artifactId>jackson-datatype-jts</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
diff --git a/tools/src/main/java/com/graphhopper/tools/Measurement.java b/tools/src/main/java/com/graphhopper/tools/Measurement.java
index 45761be32..e41e90806 100644
--- a/tools/src/main/java/com/graphhopper/tools/Measurement.java
+++ b/tools/src/main/java/com/graphhopper/tools/Measurement.java
@@ -17,6 +17,8 @@
  */
 package com.graphhopper.tools;
 
+import com.bedatadriven.jackson.datatype.jts.JtsModule;
+import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.graphhopper.*;
 import com.graphhopper.coll.GHBitSet;
@@ -24,9 +26,12 @@ import com.graphhopper.coll.GHBitSetImpl;
 import com.graphhopper.config.CHProfileConfig;
 import com.graphhopper.config.LMProfileConfig;
 import com.graphhopper.config.ProfileConfig;
+import com.graphhopper.json.geo.JsonFeatureCollection;
 import com.graphhopper.reader.DataReader;
 import com.graphhopper.reader.osm.GraphHopperOSM;
 import com.graphhopper.routing.util.*;
+import com.graphhopper.routing.util.spatialrules.*;
+import com.graphhopper.routing.util.spatialrules.SpatialRuleLookupBuilder.SpatialRuleFactory;
 import com.graphhopper.storage.*;
 import com.graphhopper.storage.index.LocationIndex;
 import com.graphhopper.util.*;
@@ -34,13 +39,20 @@ import com.graphhopper.util.Parameters.Algorithms;
 import com.graphhopper.util.Parameters.CH;
 import com.graphhopper.util.Parameters.Landmark;
 import com.graphhopper.util.shapes.BBox;
+import com.graphhopper.util.shapes.GHPoint;
+
+import org.locationtech.jts.geom.Polygon;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.text.SimpleDateFormat;
 import java.util.*;
@@ -74,6 +86,7 @@ public class Measurement {
     // Every value is one y-value in a separate diagram with an identical x-value for every Measurement.start call
     void start(PMap args) throws IOException {
         final String graphLocation = args.get("graph.location", "");
+        final String countryBordersDirectory = args.get("spatial_rules.borders_directory", "");
         final boolean useJson = args.getBool("measurement.json", false);
         boolean cleanGraph = args.getBool("measurement.clean", false);
         String summaryLocation = args.get("measurement.summaryfile", "");
@@ -232,6 +245,10 @@ public class Measurement {
                             edgeBased().withInstructions().withPointHints().simplify());
                 }
             }
+            if (!isEmpty(countryBordersDirectory)) {
+                printSpatialRuleLookupTest(countryBordersDirectory, count * 100);
+            }
+            
         } catch (Exception ex) {
             logger.error("Problem while measuring " + graphLocation, ex);
             put("error", ex.toString());
@@ -456,6 +473,57 @@ public class Measurement {
         }.setIterations(count).start();
         print("unit_tests" + description + ".get_edge_state", miniPerf);
     }
+    
+    private void printSpatialRuleLookupTest(String countryBordersDirectory, int count) {
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.registerModule(new JtsModule());
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+        
+        List<JsonFeatureCollection> jsonFeatureCollections = new ArrayList<>();
+        try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(countryBordersDirectory), "*.{geojson,json}")) {
+            for (Path borderFile : stream) {
+                try (BufferedReader reader = Files.newBufferedReader(borderFile, StandardCharsets.UTF_8)) {
+                    JsonFeatureCollection jsonFeatureCollection = objectMapper.readValue(reader, JsonFeatureCollection.class);
+                    jsonFeatureCollections.add(jsonFeatureCollection);
+                }
+            }
+        } catch (IOException e) {
+            logger.error("Failed to load borders.", e);
+            return;
+        }
+        
+        SpatialRuleFactory rulePerCountryFactory = new SpatialRuleFactory() {
+            
+            @Override
+            public SpatialRule createSpatialRule(final String id, final List<Polygon> borders) {
+                return new AbstractSpatialRule(borders) {
+                    @Override
+                    public String getId() {
+                        return id;
+                    }
+                };
+            }
+        };
+        
+        final SpatialRuleLookup spatialRuleLookup = SpatialRuleLookupBuilder.buildIndex(jsonFeatureCollections, "ISO_A3", rulePerCountryFactory);
+    
+        // generate random points in central Europe
+        final List<GHPoint> randomPoints = new ArrayList<>(count);
+        for (int i = 0; i < count; i++) {
+            double lat = 46d + Math.random() * 7d;
+            double lon = 6d + Math.random() * 21d;
+            randomPoints.add(new GHPoint(lat, lon));
+        }
+        
+        MiniPerfTest lookupPerfTest = new MiniPerfTest() {
+            @Override
+            public int doCalc(boolean warmup, int run) {
+                return spatialRuleLookup.lookupRule(randomPoints.get(run)).hashCode();
+            }
+        }.setIterations(count).start();
+        
+        print("spatialrulelookup", lookupPerfTest);
+    }
 
     private void compareRouting(final GraphHopper hopper, String vehicle, int count) {
         logger.info("Comparing " + count + " routes. Differences will be printed to stderr.");
diff --git a/tools/src/main/java/com/graphhopper/ui/MiniGraphUI.java b/tools/src/main/java/com/graphhopper/ui/MiniGraphUI.java
index a8cb0d119..d3bc6e076 100644
--- a/tools/src/main/java/com/graphhopper/ui/MiniGraphUI.java
+++ b/tools/src/main/java/com/graphhopper/ui/MiniGraphUI.java
@@ -22,6 +22,9 @@ import com.graphhopper.GraphHopper;
 import com.graphhopper.GraphHopperConfig;
 import com.graphhopper.coll.GHBitSet;
 import com.graphhopper.coll.GHTBitSet;
+import com.graphhopper.config.CHProfileConfig;
+import com.graphhopper.config.LMProfileConfig;
+import com.graphhopper.config.ProfileConfig;
 import com.graphhopper.reader.osm.GraphHopperOSM;
 import com.graphhopper.routing.*;
 import com.graphhopper.routing.ch.CHRoutingAlgorithmFactory;
@@ -31,13 +34,11 @@ import com.graphhopper.routing.querygraph.QueryGraph;
 import com.graphhopper.routing.util.AllEdgesIterator;
 import com.graphhopper.routing.util.EdgeFilter;
 import com.graphhopper.routing.util.FlagEncoder;
-import com.graphhopper.routing.util.HintsMap;
 import com.graphhopper.routing.weighting.Weighting;
 import com.graphhopper.storage.*;
 import com.graphhopper.storage.index.LocationIndexTree;
 import com.graphhopper.storage.index.QueryResult;
 import com.graphhopper.util.PMap;
-import com.graphhopper.util.Parameters;
 import com.graphhopper.util.Parameters.Algorithms;
 import com.graphhopper.util.PointList;
 import com.graphhopper.util.StopWatch;
@@ -51,10 +52,9 @@ import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseWheelEvent;
 import java.awt.event.MouseWheelListener;
+import java.util.Arrays;
 import java.util.Random;
 
-import static com.graphhopper.routing.weighting.TurnCostProvider.NO_TURN_COST_PROVIDER;
-
 /**
  * A rough graphical user interface for visualizing the OSM graph. Mainly for debugging algorithms
  * and spatial data structures. See e.g. this blog post:
@@ -97,17 +97,16 @@ public class MiniGraphUI {
         encoder = hopper.getEncodingManager().getEncoder("car");
         avSpeedEnc = encoder.getAverageSpeedEnc();
         accessEnc = encoder.getAccessEnc();
-        HintsMap map = new HintsMap("fastest").
-                setVehicle("car");
-
+        ProfileConfig profile = hopper.getProfiles().iterator().next();
         boolean ch = true;
         if (ch) {
-            map.put(Parameters.Landmark.DISABLE, true);
             CHProfile chProfile = hopper.getCHPreparationHandler().getNodeBasedCHProfiles().get(0);
             weighting = chProfile.getWeighting();
             routingGraph = hopper.getGraphHopperStorage().getCHGraph(chProfile);
 
-            final RoutingAlgorithmFactory tmpFactory = hopper.getAlgorithmFactory(map);
+            boolean disableCH = false;
+            boolean disableLM = true;
+            final RoutingAlgorithmFactory tmpFactory = hopper.getAlgorithmFactory(profile.getName(), disableCH, disableLM);
             algoFactory = new RoutingAlgorithmFactory() {
 
                 class TmpAlgo extends DijkstraBidirectionCH implements DebugAlgo {
@@ -143,11 +142,11 @@ public class MiniGraphUI {
             algoOpts = new AlgorithmOptions(Algorithms.DIJKSTRA_BI, weighting);
 
         } else {
-            map.put(Parameters.CH.DISABLE, true);
-//            map.put(Parameters.Landmark.DISABLE, true);
             routingGraph = graph;
-            weighting = hopper.createWeighting(map, encoder, NO_TURN_COST_PROVIDER);
-            final RoutingAlgorithmFactory tmpFactory = hopper.getAlgorithmFactory(map);
+            weighting = hopper.createWeighting(profile, new PMap());
+            boolean disableCH = true;
+            boolean disableLM = false;
+            final RoutingAlgorithmFactory tmpFactory = hopper.getAlgorithmFactory(profile.getName(), disableCH, disableLM);
             algoFactory = new RoutingAlgorithmFactory() {
 
                 @Override
@@ -385,9 +384,20 @@ public class MiniGraphUI {
         }
     }
 
-    public static void main(String[] strs) throws Exception {
+    public static void main(String[] strs) {
         PMap args = PMap.read(strs);
         GraphHopperConfig ghConfig = new GraphHopperConfig(args);
+        ghConfig.setProfiles(Arrays.asList(
+                new ProfileConfig("profile")
+                        .setVehicle("car")
+                        .setWeighting("fastest")
+        ));
+        ghConfig.setCHProfiles(Arrays.asList(
+                new CHProfileConfig("profile")
+        ));
+        ghConfig.setLMProfiles(Arrays.asList(
+                new LMProfileConfig("profile")
+        ));
         GraphHopper hopper = new GraphHopperOSM().init(ghConfig).importOrLoad();
         boolean debug = args.getBool("minigraphui.debug", false);
         new MiniGraphUI(hopper, debug).visualize();
diff --git a/web-bundle/src/main/java/com/graphhopper/resources/IsochroneResource.java b/web-bundle/src/main/java/com/graphhopper/resources/IsochroneResource.java
index 78dced27b..278117e59 100644
--- a/web-bundle/src/main/java/com/graphhopper/resources/IsochroneResource.java
+++ b/web-bundle/src/main/java/com/graphhopper/resources/IsochroneResource.java
@@ -3,6 +3,7 @@ package com.graphhopper.resources;
 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.graphhopper.GraphHopper;
+import com.graphhopper.config.ProfileConfig;
 import com.graphhopper.http.WebHelper;
 import com.graphhopper.isochrone.algorithm.ContourBuilder;
 import com.graphhopper.isochrone.algorithm.Isochrone;
@@ -34,8 +35,6 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 import java.util.*;
 
-import static com.graphhopper.routing.weighting.TurnCostProvider.NO_TURN_COST_PROVIDER;
-
 @Path("isochrone")
 public class IsochroneResource {
 
@@ -55,7 +54,6 @@ public class IsochroneResource {
     @Produces({MediaType.APPLICATION_JSON})
     public Response doGet(
             @Context UriInfo uriInfo,
-            @QueryParam("vehicle") @DefaultValue("car") String vehicle,
             @QueryParam("buckets") @DefaultValue("1") int nBuckets,
             @QueryParam("reverse_flow") @DefaultValue("false") boolean reverseFlow,
             @QueryParam("point") GHPoint point,
@@ -71,13 +69,18 @@ public class IsochroneResource {
 
         StopWatch sw = new StopWatch().start();
 
-        if (!encodingManager.hasEncoder(vehicle))
-            throw new IllegalArgumentException("vehicle not supported:" + vehicle);
-
         if (respType != null && !respType.equalsIgnoreCase("json") && !respType.equalsIgnoreCase("geojson"))
             throw new IllegalArgumentException("Format not supported:" + respType);
 
-        FlagEncoder encoder = encodingManager.getEncoder(vehicle);
+        HintsMap hintsMap = new HintsMap();
+        RouteResource.initHints(hintsMap, uriInfo.getQueryParameters());
+        hintsMap.put(Parameters.CH.DISABLE, true);
+        hintsMap.put(Parameters.Landmark.DISABLE, true);
+        ProfileConfig profile = graphHopper.resolveProfile(hintsMap);
+        if (profile.isTurnCosts()) {
+            throw new IllegalArgumentException("Isochrone calculation does not support turn costs yet");
+        }
+        FlagEncoder encoder = encodingManager.getEncoder(profile.getVehicle());
         EdgeFilter edgeFilter = DefaultEdgeFilter.allEdges(encoder);
         LocationIndex locationIndex = graphHopper.getLocationIndex();
         QueryResult qr = locationIndex.findClosest(point.lat, point.lon, edgeFilter);
@@ -87,11 +90,7 @@ public class IsochroneResource {
         Graph graph = graphHopper.getGraphHopperStorage();
         QueryGraph queryGraph = QueryGraph.lookup(graph, qr);
 
-        HintsMap hintsMap = new HintsMap();
-        RouteResource.initHints(hintsMap, uriInfo.getQueryParameters());
-
-        // todo: isochrones with turn costs ?
-        Weighting weighting = graphHopper.createWeighting(hintsMap, encoder, NO_TURN_COST_PROVIDER);
+        Weighting weighting = graphHopper.createWeighting(profile, hintsMap);
         if (hintsMap.has(Parameters.Routing.BLOCK_AREA))
             weighting = new BlockAreaWeighting(weighting, GraphEdgeIdFinder.createBlockArea(graph, locationIndex,
                     Collections.singletonList(point), hintsMap, DefaultEdgeFilter.allEdges(encoder)));
diff --git a/web-bundle/src/main/java/com/graphhopper/resources/SPTResource.java b/web-bundle/src/main/java/com/graphhopper/resources/SPTResource.java
index 6a669d9e0..aa5306886 100644
--- a/web-bundle/src/main/java/com/graphhopper/resources/SPTResource.java
+++ b/web-bundle/src/main/java/com/graphhopper/resources/SPTResource.java
@@ -1,6 +1,7 @@
 package com.graphhopper.resources;
 
 import com.graphhopper.GraphHopper;
+import com.graphhopper.config.ProfileConfig;
 import com.graphhopper.isochrone.algorithm.Isochrone;
 import com.graphhopper.routing.profiles.*;
 import com.graphhopper.routing.querygraph.QueryGraph;
@@ -28,8 +29,6 @@ import java.io.OutputStreamWriter;
 import java.io.Writer;
 import java.util.*;
 
-import static com.graphhopper.routing.weighting.TurnCostProvider.NO_TURN_COST_PROVIDER;
-
 /**
  * This resource provides the entire shortest path tree as response. In a simple CSV format discussed at #1577.
  */
@@ -51,7 +50,6 @@ public class SPTResource {
     @Produces("text/csv")
     public Response doGet(
             @Context UriInfo uriInfo,
-            @QueryParam("vehicle") @DefaultValue("car") String vehicle,
             @QueryParam("reverse_flow") @DefaultValue("false") boolean reverseFlow,
             @QueryParam("point") GHPoint point,
             @QueryParam("columns") String columnsParam,
@@ -62,11 +60,15 @@ public class SPTResource {
             throw new IllegalArgumentException("point parameter cannot be null");
 
         StopWatch sw = new StopWatch().start();
-
-        if (!encodingManager.hasEncoder(vehicle))
-            throw new IllegalArgumentException("vehicle not supported:" + vehicle);
-
-        FlagEncoder encoder = encodingManager.getEncoder(vehicle);
+        HintsMap hintsMap = new HintsMap();
+        RouteResource.initHints(hintsMap, uriInfo.getQueryParameters());
+        hintsMap.put(Parameters.CH.DISABLE, true);
+        hintsMap.put(Parameters.Landmark.DISABLE, true);
+        ProfileConfig profile = graphHopper.resolveProfile(hintsMap);
+        if (profile.isTurnCosts()) {
+            throw new IllegalArgumentException("SPT calculation does not support turn costs yet");
+        }
+        FlagEncoder encoder = encodingManager.getEncoder(profile.getVehicle());
         EdgeFilter edgeFilter = DefaultEdgeFilter.allEdges(encoder);
         LocationIndex locationIndex = graphHopper.getLocationIndex();
         QueryResult qr = locationIndex.findClosest(point.lat, point.lon, edgeFilter);
@@ -75,11 +77,8 @@ public class SPTResource {
 
         Graph graph = graphHopper.getGraphHopperStorage();
         QueryGraph queryGraph = QueryGraph.lookup(graph, qr);
-        HintsMap hintsMap = new HintsMap();
-        RouteResource.initHints(hintsMap, uriInfo.getQueryParameters());
 
-        // todo: /spt with turn costs ?
-        Weighting weighting = graphHopper.createWeighting(hintsMap, encoder, NO_TURN_COST_PROVIDER);
+        Weighting weighting = graphHopper.createWeighting(profile, hintsMap);
         if (hintsMap.has(Parameters.Routing.BLOCK_AREA))
             weighting = new BlockAreaWeighting(weighting, GraphEdgeIdFinder.createBlockArea(graph, locationIndex,
                     Collections.singletonList(point), hintsMap, DefaultEdgeFilter.allEdges(encoder)));


Test patch:
diff --git a/core/src/main/java/com/graphhopper/util/MiniPerfTest.java b/core/src/main/java/com/graphhopper/util/MiniPerfTest.java
index ffe3dcfc4..2cda3fec5 100644
--- a/core/src/main/java/com/graphhopper/util/MiniPerfTest.java
+++ b/core/src/main/java/com/graphhopper/util/MiniPerfTest.java
@@ -28,11 +28,16 @@ import java.util.Locale;
  * @author Peter Karich
  */
 public abstract class MiniPerfTest {
+
+    private static final double NS_PER_S  = 1e9;
+    private static final double NS_PER_MS = 1e6;
+    private static final double NS_PER_US = 1e3;
+    
     protected Logger logger = LoggerFactory.getLogger(getClass());
     private int counts = 100;
-    private double fullTime = 0;
-    private double max;
-    private double min = Double.MAX_VALUE;
+    private long fullTime = 0;
+    private long max;
+    private long min = Long.MAX_VALUE;
     private int dummySum;
 
     public MiniPerfTest start() {
@@ -65,21 +70,21 @@ public abstract class MiniPerfTest {
      * @return minimum time of every call, in ms
      */
     public double getMin() {
-        return min / 1e6;
+        return min / NS_PER_MS;
     }
 
     /**
      * @return maximum time of every calls, in ms
      */
     public double getMax() {
-        return max / 1e6;
+        return max / NS_PER_MS;
     }
 
     /**
      * @return time for all calls accumulated, in ms
      */
     public double getSum() {
-        return fullTime / 1e6;
+        return fullTime / NS_PER_MS;
     }
 
     /**
@@ -88,16 +93,33 @@ public abstract class MiniPerfTest {
     public double getMean() {
         return getSum() / counts;
     }
+    
+    private String formatDuration(double durationNs) {
+        double divisor;
+        String unit;
+        if (durationNs > 1e7d) {
+            divisor = NS_PER_S;
+            unit = "s";
+        } else if (durationNs > 1e4d) {
+            divisor = NS_PER_MS;
+            unit = "ms";
+        } else {
+            divisor = NS_PER_US;
+            unit = "us";
+        }
+        return nf(durationNs / divisor) + unit;
+    }
 
     public String getReport() {
-        return "sum:" + nf(getSum() / 1000f) + "s, time/call:" + nf(getMean() / 1000f) + "s";
+        double meanNs = ((double) fullTime) / counts;
+        return "sum:" + formatDuration(fullTime) + ", time/call:" + formatDuration(meanNs);
     }
 
     public int getDummySum() {
         return dummySum;
     }
 
-    public String nf(Number num) {
+    private String nf(Number num) {
         return new DecimalFormat("#.###", DecimalFormatSymbols.getInstance(Locale.ROOT)).format(num);
     }
 
diff --git a/core/src/test/java/com/graphhopper/GraphHopperProfileConfigTest.java b/core/src/test/java/com/graphhopper/GraphHopperProfileConfigTest.java
index d985814e6..31af3c41a 100644
--- a/core/src/test/java/com/graphhopper/GraphHopperProfileConfigTest.java
+++ b/core/src/test/java/com/graphhopper/GraphHopperProfileConfigTest.java
@@ -28,6 +28,7 @@ import com.graphhopper.routing.util.EncodingManager;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
+import java.util.Arrays;
 
 import static org.junit.jupiter.api.Assertions.*;
 
@@ -53,7 +54,7 @@ public class GraphHopperProfileConfigTest {
     @Test
     public void duplicateProfileName_error() {
         final GraphHopper hopper = createHopper(EncodingManager.create("car"));
-        assertIllegalArgument("Profile names must be unique. Duplicate name: 'my_profile'", new Runnable() {
+        assertIllegalArgument(new Runnable() {
             @Override
             public void run() {
                 hopper.setProfiles(
@@ -62,43 +63,46 @@ public class GraphHopperProfileConfigTest {
                         new ProfileConfig("my_profile").setVehicle("car").setWeighting("shortest")
                 );
             }
-        });
+        }, "Profile names must be unique. Duplicate name: 'my_profile'");
     }
 
     @Test
     public void vehicleDoesNotExist_error() {
         final GraphHopper hopper = createHopper(EncodingManager.create("car"));
         hopper.setProfiles(new ProfileConfig("profile").setVehicle("your_car"));
-        assertIllegalArgument("Unknown vehicle 'your_car' in profile: name=profile", new Runnable() {
+        assertIllegalArgument(new Runnable() {
             @Override
             public void run() {
                 hopper.load(GH_LOCATION);
             }
-        });
+        }, "Unknown vehicle 'your_car' in profile: name=profile");
     }
 
     @Test
     public void vehicleWithoutTurnCostSupport_error() {
         final GraphHopper hopper = createHopper(EncodingManager.create("car"));
         hopper.setProfiles(new ProfileConfig("profile").setVehicle("car").setTurnCosts(true));
-        assertIllegalArgument("The profile 'profile' was configured with 'turn_costs=true', but the corresponding vehicle 'car' does not support turn costs", new Runnable() {
+        assertIllegalArgument(new Runnable() {
             @Override
             public void run() {
                 hopper.load(GH_LOCATION);
             }
-        });
+        }, "The profile 'profile' was configured with 'turn_costs=true', but the corresponding vehicle 'car' does not support turn costs");
     }
 
     @Test
     public void profileWithUnknownWeighting_error() {
         final GraphHopper hopper = createHopper(EncodingManager.create("car"));
         hopper.setProfiles(new ProfileConfig("profile").setVehicle("car").setWeighting("your_weighting"));
-        assertIllegalArgument("The profile 'profile' was configured with an unknown weighting 'your_weighting'", new Runnable() {
-            @Override
-            public void run() {
-                hopper.load(GH_LOCATION);
-            }
-        });
+        assertIllegalArgument(new Runnable() {
+                                  @Override
+                                  public void run() {
+                                      hopper.load(GH_LOCATION);
+                                  }
+                              },
+                "Could not create weighting for profile: 'profile'",
+                "Weighting 'your_weighting' not supported"
+        );
     }
 
     @Test
@@ -106,12 +110,12 @@ public class GraphHopperProfileConfigTest {
         final GraphHopper hopper = createHopper(EncodingManager.create("car"));
         hopper.setProfiles(new ProfileConfig("profile1").setVehicle("car"));
         hopper.getCHPreparationHandler().setCHProfileConfigs(new CHProfileConfig("other_profile"));
-        assertIllegalArgument("CH profile references unknown profile 'other_profile'", new Runnable() {
+        assertIllegalArgument(new Runnable() {
             @Override
             public void run() {
                 hopper.load(GH_LOCATION);
             }
-        });
+        }, "CH profile references unknown profile 'other_profile'");
     }
 
     @Test
@@ -122,12 +126,12 @@ public class GraphHopperProfileConfigTest {
                 new CHProfileConfig("profile"),
                 new CHProfileConfig("profile")
         );
-        assertIllegalArgument("Duplicate CH reference to profile 'profile'", new Runnable() {
+        assertIllegalArgument(new Runnable() {
             @Override
             public void run() {
                 hopper.load(GH_LOCATION);
             }
-        });
+        }, "Duplicate CH reference to profile 'profile'");
     }
 
     @Test
@@ -135,12 +139,12 @@ public class GraphHopperProfileConfigTest {
         final GraphHopper hopper = createHopper(EncodingManager.create("car"));
         hopper.setProfiles(new ProfileConfig("profile1").setVehicle("car"));
         hopper.getLMPreparationHandler().setLMProfileConfigs(new LMProfileConfig("other_profile"));
-        assertIllegalArgument("LM profile references unknown profile 'other_profile'", new Runnable() {
+        assertIllegalArgument(new Runnable() {
             @Override
             public void run() {
                 hopper.load(GH_LOCATION);
             }
-        });
+        }, "LM profile references unknown profile 'other_profile'");
     }
 
     @Test
@@ -151,12 +155,12 @@ public class GraphHopperProfileConfigTest {
                 new LMProfileConfig("profile"),
                 new LMProfileConfig("profile")
         );
-        assertIllegalArgument("Duplicate LM reference to profile 'profile'", new Runnable() {
+        assertIllegalArgument(new Runnable() {
             @Override
             public void run() {
                 hopper.load(GH_LOCATION);
             }
-        });
+        }, "Duplicate LM reference to profile 'profile'");
     }
 
 
@@ -168,12 +172,14 @@ public class GraphHopperProfileConfigTest {
         return hopper;
     }
 
-    private static void assertIllegalArgument(String messagePart, Runnable runnable) {
+    private static void assertIllegalArgument(Runnable runnable, String... messageParts) {
         try {
             runnable.run();
-            fail("There should have been an error containing:\n\t" + messagePart);
+            fail("There should have been an error containing:\n\t" + Arrays.asList(messageParts));
         } catch (IllegalArgumentException e) {
-            assertTrue(e.getMessage().contains(messagePart), "Unexpected error message:\n\t" + e.getMessage() + "\nExpected the message to contain:\n\t" + messagePart);
+            for (String messagePart : messageParts) {
+                assertTrue(e.getMessage().contains(messagePart), "Unexpected error message:\n\t" + e.getMessage() + "\nExpected the message to contain:\n\t" + messagePart);
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/RoutingAlgorithmIT.java b/core/src/test/java/com/graphhopper/routing/RoutingAlgorithmIT.java
index bd78dbda8..b4ea4eec7 100644
--- a/core/src/test/java/com/graphhopper/routing/RoutingAlgorithmIT.java
+++ b/core/src/test/java/com/graphhopper/routing/RoutingAlgorithmIT.java
@@ -18,15 +18,12 @@
 package com.graphhopper.routing;
 
 import com.graphhopper.GraphHopper;
+import com.graphhopper.config.ProfileConfig;
 import com.graphhopper.reader.PrincetonReader;
-import com.graphhopper.routing.ch.CHProfileSelector;
 import com.graphhopper.routing.util.EncodingManager;
-import com.graphhopper.routing.util.FlagEncoder;
 import com.graphhopper.routing.util.HintsMap;
 import com.graphhopper.routing.util.TestAlgoCollector.AlgoHelperEntry;
 import com.graphhopper.routing.util.TraversalMode;
-import com.graphhopper.routing.weighting.DefaultTurnCostProvider;
-import com.graphhopper.routing.weighting.TurnCostProvider;
 import com.graphhopper.routing.weighting.Weighting;
 import com.graphhopper.storage.CHProfile;
 import com.graphhopper.storage.Directory;
@@ -45,7 +42,6 @@ import java.util.List;
 import java.util.Random;
 import java.util.zip.GZIPInputStream;
 
-import static com.graphhopper.routing.weighting.TurnCostProvider.NO_TURN_COST_PROVIDER;
 import static com.graphhopper.util.Parameters.Algorithms.*;
 import static org.junit.Assert.assertTrue;
 
@@ -63,9 +59,8 @@ public class RoutingAlgorithmIT {
         if (tMode.isEdgeBased())
             addStr = "turn|";
 
-        FlagEncoder encoder = hopper.getEncodingManager().getEncoder(hints.getVehicle());
-        TurnCostProvider turnCostProvider = tMode.isEdgeBased() ? new DefaultTurnCostProvider(encoder, ghStorage.getTurnCostStorage()) : NO_TURN_COST_PROVIDER;
-        Weighting weighting = hopper.createWeighting(hints, encoder, turnCostProvider);
+        ProfileConfig profile = new ProfileConfig("profile").setVehicle(hints.getVehicle()).setWeighting(hints.getWeighting()).setTurnCosts(tMode.isEdgeBased());
+        Weighting weighting = hopper.createWeighting(profile, hints);
 
         HintsMap defaultHints = new HintsMap().put(Parameters.CH.DISABLE, true).put(Parameters.Landmark.DISABLE, true)
                 .setVehicle(hints.getVehicle()).setWeighting(hints.getWeighting());
@@ -88,7 +83,7 @@ public class RoutingAlgorithmIT {
             algos.add(new AlgoHelperEntry(ghStorage, AlgorithmOptions.start(astarbiOpts).hints(lmHints).build(), idx, "astarbi|landmarks|" + weighting) {
                 @Override
                 public RoutingAlgorithmFactory createRoutingFactory() {
-                    return hopper.getAlgorithmFactory(lmHints);
+                    return hopper.getAlgorithmFactory(lmHints.getVehicle() + "_profile", true, false);
                 }
             });
         }
@@ -97,12 +92,12 @@ public class RoutingAlgorithmIT {
             final HintsMap chHints = new HintsMap(defaultHints);
             chHints.put(Parameters.CH.DISABLE, false);
             chHints.put(Parameters.Routing.EDGE_BASED, tMode.isEdgeBased());
-            CHProfile pickedProfile = CHProfileSelector.select(hopper.getCHPreparationHandler().getCHProfiles(), chHints);
+            CHProfile pickedProfile = new ProfileResolver().selectCHProfile(hopper.getCHPreparationHandler().getCHProfiles(), chHints);
             algos.add(new AlgoHelperEntry(ghStorage.getCHGraph(pickedProfile),
                     AlgorithmOptions.start(dijkstrabiOpts).hints(chHints).build(), idx, "dijkstrabi|ch|algos|" + hints.getWeighting()) {
                 @Override
                 public RoutingAlgorithmFactory createRoutingFactory() {
-                    return hopper.getAlgorithmFactory(chHints);
+                    return hopper.getAlgorithmFactory(chHints.getVehicle() + "_profile", false, true);
                 }
             });
 
@@ -110,7 +105,7 @@ public class RoutingAlgorithmIT {
                     AlgorithmOptions.start(astarbiOpts).hints(chHints).build(), idx, "astarbi|ch|algos|" + hints.getWeighting()) {
                 @Override
                 public RoutingAlgorithmFactory createRoutingFactory() {
-                    return hopper.getAlgorithmFactory(chHints);
+                    return hopper.getAlgorithmFactory(chHints.getVehicle() + "_profile", false, true);
                 }
             });
         }
diff --git a/core/src/test/java/com/graphhopper/routing/RoutingAlgorithmTest.java b/core/src/test/java/com/graphhopper/routing/RoutingAlgorithmTest.java
index 9f1f0a926..3ae6adde3 100644
--- a/core/src/test/java/com/graphhopper/routing/RoutingAlgorithmTest.java
+++ b/core/src/test/java/com/graphhopper/routing/RoutingAlgorithmTest.java
@@ -23,7 +23,10 @@ import com.graphhopper.routing.ch.PrepareContractionHierarchies;
 import com.graphhopper.routing.profiles.BooleanEncodedValue;
 import com.graphhopper.routing.profiles.DecimalEncodedValue;
 import com.graphhopper.routing.querygraph.QueryGraph;
-import com.graphhopper.routing.util.*;
+import com.graphhopper.routing.util.EdgeFilter;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.routing.util.FlagEncoder;
+import com.graphhopper.routing.util.TraversalMode;
 import com.graphhopper.routing.weighting.FastestWeighting;
 import com.graphhopper.routing.weighting.ShortestWeighting;
 import com.graphhopper.routing.weighting.TurnCostProvider;
@@ -853,11 +856,6 @@ public class RoutingAlgorithmTest {
                 return tmpW.calcTurnMillis(inEdge, viaNode, outEdge);
             }
 
-            @Override
-            public boolean matches(HintsMap map) {
-                throw new UnsupportedOperationException("Not supported");
-            }
-
             @Override
             public String getName() {
                 return "custom";
diff --git a/core/src/test/java/com/graphhopper/routing/ch/CHProfileSelectorTest.java b/core/src/test/java/com/graphhopper/routing/ch/CHProfileSelectorTest.java
index 484ed028e..ed970cf1c 100644
--- a/core/src/test/java/com/graphhopper/routing/ch/CHProfileSelectorTest.java
+++ b/core/src/test/java/com/graphhopper/routing/ch/CHProfileSelectorTest.java
@@ -18,23 +18,25 @@
 
 package com.graphhopper.routing.ch;
 
+import com.graphhopper.routing.ProfileResolver;
 import com.graphhopper.routing.util.*;
 import com.graphhopper.routing.weighting.*;
 import com.graphhopper.storage.CHProfile;
 import com.graphhopper.storage.TurnCostStorage;
 import com.graphhopper.util.Parameters;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class CHProfileSelectorTest {
 
+    private static final String MULTIPLE_EDGE_BASED_ERROR = "There are multiple edge-based CH profiles matching your request. You need to specify the profile you want to use explicitly";
     private static final String MULTIPLE_MATCHES_ERROR = "There are multiple CH profiles matching your request. Use the `weighting`,`vehicle`,`edge_based` and/or `u_turn_costs` parameters to be more specific";
     private static final String NO_MATCH_ERROR = "Cannot find matching CH profile for your request";
 
@@ -48,7 +50,7 @@ public class CHProfileSelectorTest {
     private Weighting weightingShortestCar;
     private Weighting weightingShortestBike;
 
-    @Before
+    @BeforeEach
     public void setup() {
         FlagEncoder carEncoder = new CarFlagEncoder();
         FlagEncoder bikeEncoder = new BikeFlagEncoder();
@@ -76,7 +78,9 @@ public class CHProfileSelectorTest {
         assertCHProfileSelectionError(NO_MATCH_ERROR, chProfiles, false, 20);
         assertProfileFound(chProfiles.get(0), chProfiles, null, null);
         assertCHProfileSelectionError(NO_MATCH_ERROR, chProfiles, null, 30);
-        assertCHProfileSelectionError(NO_MATCH_ERROR, chProfiles, "foot", "fastest", false, null);
+        String error = assertCHProfileSelectionError(NO_MATCH_ERROR, chProfiles, "foot", "fastest", false, null);
+        assertTrue(error.contains("requested:  fastest|foot|edge_based=false|u_turn_costs=*"), error);
+        assertTrue(error.contains("available: [fastest|car|edge_based=false]"), error);
     }
 
     @Test
@@ -109,17 +113,17 @@ public class CHProfileSelectorTest {
                 CHProfile.edgeBased(weightingFastestCarEdge30),
                 CHProfile.edgeBased(weightingFastestCarEdge50)
         );
-        // when no u-turns are specified we throw
-        assertCHProfileSelectionError(MULTIPLE_MATCHES_ERROR, chProfiles, true, null);
-        // when we request one that does not exist we throw
-        assertCHProfileSelectionError(NO_MATCH_ERROR, chProfiles, true, 40);
-        // when we request one that exists it works
-        assertProfileFound(chProfiles.get(1), chProfiles, true, 30);
-
-        // without specifying edge-based
-        assertProfileFound(chProfiles.get(1), chProfiles, null, 30);
-        assertCHProfileSelectionError(NO_MATCH_ERROR, chProfiles, null, 40);
+        // since there are multiple edge-based profiles we will get an error no matter if we do not specify the u-turn
+        // costs, specify some that do not exist or even specify the right u-turn costs, because we no longer allow
+        // choosing between multiple edge-based profiles without giving the profile name explicitly
+        assertCHProfileSelectionError(MULTIPLE_EDGE_BASED_ERROR, chProfiles, true, null);
+        assertCHProfileSelectionError(MULTIPLE_EDGE_BASED_ERROR, chProfiles, true, 40);
+        assertCHProfileSelectionError(MULTIPLE_EDGE_BASED_ERROR, chProfiles, true, 30);
+
+        // without specifying edge-based we also get an error
         assertCHProfileSelectionError(MULTIPLE_MATCHES_ERROR, chProfiles, null, null);
+        assertCHProfileSelectionError(MULTIPLE_EDGE_BASED_ERROR, chProfiles, null, 40);
+        assertCHProfileSelectionError(MULTIPLE_EDGE_BASED_ERROR, chProfiles, null, 30);
     }
 
     @Test
@@ -173,9 +177,9 @@ public class CHProfileSelectorTest {
         // if we do not specify the vehicle but edge_based=false its clear what to return because for node-based there is only one weighting
         assertProfileFound(chProfiles.get(0), chProfiles, "", "", false, null);
         // ... for edge_based=true this is an error, because there are two edge_based profiles
-        assertCHProfileSelectionError(MULTIPLE_MATCHES_ERROR, chProfiles, "", "", true, null);
-        // .. we can however get a clear match if we specify the u-turn costs
-        assertProfileFound(chProfiles.get(1), chProfiles, "", "", true, 10);
+        assertCHProfileSelectionError(MULTIPLE_EDGE_BASED_ERROR, chProfiles, "", "", true, null);
+        // ... and its even an error if we specify the (correct) u-turn costs, because we no longer allow distinguishing the profile this way
+        assertCHProfileSelectionError(MULTIPLE_EDGE_BASED_ERROR, chProfiles, "", "", true, 10);
     }
 
     @Test
@@ -219,25 +223,27 @@ public class CHProfileSelectorTest {
     private void assertProfileFound(CHProfile expectedProfile, List<CHProfile> profiles, String vehicle, String weighting, Boolean edgeBased, Integer uTurnCosts) {
         HintsMap hintsMap = createHintsMap(vehicle, weighting, edgeBased, uTurnCosts);
         try {
-            CHProfile selectedProfile = CHProfileSelector.select(profiles, hintsMap);
+            CHProfile selectedProfile = new ProfileResolver().selectCHProfile(profiles, hintsMap);
             assertEquals(expectedProfile, selectedProfile);
-        } catch (CHProfileSelectionException e) {
+        } catch (IllegalArgumentException e) {
             fail("no profile found\nexpected: " + expectedProfile + "\nerror: " + e.getMessage());
         }
     }
 
-    private void assertCHProfileSelectionError(String expectedError, List<CHProfile> profiles, Boolean edgeBased, Integer uTurnCosts) {
-        assertCHProfileSelectionError(expectedError, profiles, "car", "fastest", edgeBased, uTurnCosts);
+    private String assertCHProfileSelectionError(String expectedError, List<CHProfile> profiles, Boolean edgeBased, Integer uTurnCosts) {
+        return assertCHProfileSelectionError(expectedError, profiles, "car", "fastest", edgeBased, uTurnCosts);
     }
 
-    private void assertCHProfileSelectionError(String expectedError, List<CHProfile> profiles, String vehicle, String weighting, Boolean edgeBased, Integer uTurnCosts) {
+    private String assertCHProfileSelectionError(String expectedError, List<CHProfile> profiles, String vehicle, String weighting, Boolean edgeBased, Integer uTurnCosts) {
         HintsMap hintsMap = createHintsMap(vehicle, weighting, edgeBased, uTurnCosts);
         try {
-            CHProfileSelector.select(profiles, hintsMap);
+            new ProfileResolver().selectCHProfile(profiles, hintsMap);
             fail("There should have been an error");
-        } catch (CHProfileSelectionException e) {
-            assertTrue("There should have been an error message containing:\n'" + expectedError + "'\nbut was:\n'" + e.getMessage() + "'",
-                    e.getMessage().contains(expectedError));
+            return "";
+        } catch (IllegalArgumentException e) {
+            assertTrue(e.getMessage().contains(expectedError),
+                    "There should have been an error message containing:\n'" + expectedError + "'\nbut was:\n'" + e.getMessage() + "'");
+            return e.getMessage();
         }
     }
 
diff --git a/core/src/test/java/com/graphhopper/routing/lm/LMProfileSelectorTest.java b/core/src/test/java/com/graphhopper/routing/lm/LMProfileSelectorTest.java
new file mode 100644
index 000000000..a97d47dd8
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/lm/LMProfileSelectorTest.java
@@ -0,0 +1,141 @@
+/*
+ *  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.lm;
+
+import com.graphhopper.routing.ProfileResolver;
+import com.graphhopper.routing.util.*;
+import com.graphhopper.routing.weighting.FastestWeighting;
+import com.graphhopper.routing.weighting.Weighting;
+import com.graphhopper.util.Parameters;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+
+public class LMProfileSelectorTest {
+
+    private static final String MULTIPLE_MATCHES_ERROR = "There are multiple LM profiles matching your request. Use the `weighting` and `vehicle` parameters to be more specific";
+    private static final String NO_MATCH_ERROR = "Cannot find matching LM profile for your request";
+
+    private Weighting weightingFastestCar;
+    private Weighting weightingFastestBike;
+
+    @BeforeEach
+    public void setup() {
+        FlagEncoder carEncoder = new CarFlagEncoder();
+        FlagEncoder bikeEncoder = new BikeFlagEncoder();
+        EncodingManager.create(carEncoder, bikeEncoder);
+        weightingFastestCar = new FastestWeighting(carEncoder);
+        weightingFastestBike = new FastestWeighting(bikeEncoder);
+    }
+
+    @Test
+    public void singleProfile() {
+        List<LMProfile> lmProfiles = Arrays.asList(
+                new LMProfile("profile1", weightingFastestCar)
+        );
+        // as long as we do not request something that does not fit the existing profile we have a match
+        assertProfileFound(lmProfiles.get(0), lmProfiles, null, null, null, null);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, "car", null, null, null);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, null, "fastest", null, null);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, "car", "fastest", null, null);
+
+        // requesting edge_based or u_turn_costs should not lead to a non-match
+        assertProfileFound(lmProfiles.get(0), lmProfiles, null, null, true, null);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, null, null, false, null);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, null, null, null, 54);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, null, null, true, 54);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, "car", null, true, 54);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, null, "fastest", true, 54);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, "car", "fastest", true, null);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, "car", "fastest", true, 54);
+
+        // if we request something that does not fit we do not get a match
+        String error = assertLMProfileSelectionError(NO_MATCH_ERROR, lmProfiles, "bike", null, null, null);
+        assertTrue(error.contains("available: [fastest|car]"), error);
+        assertLMProfileSelectionError(NO_MATCH_ERROR, lmProfiles, "car", "shortest", null, null);
+        assertLMProfileSelectionError(NO_MATCH_ERROR, lmProfiles, null, "shortest", null, null);
+        assertLMProfileSelectionError(NO_MATCH_ERROR, lmProfiles, "truck", "short_fastest", null, null);
+    }
+
+    @Test
+    public void multipleProfiles() {
+        List<LMProfile> lmProfiles = Arrays.asList(
+                new LMProfile("profile1", weightingFastestCar),
+                new LMProfile("profile2", weightingFastestBike)
+        );
+
+        assertProfileFound(lmProfiles.get(0), lmProfiles, "car", null, null, null);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, "car", "fastest", null, null);
+        assertProfileFound(lmProfiles.get(1), lmProfiles, "bike", null, null, null);
+        assertProfileFound(lmProfiles.get(1), lmProfiles, "bike", "fastest", null, null);
+
+        // not specific enough
+        String error = assertLMProfileSelectionError(MULTIPLE_MATCHES_ERROR, lmProfiles, null, "fastest", null, null);
+        assertTrue(error.contains("requested:  fastest|*"), error);
+        assertTrue(error.contains("matched:   [fastest|car, fastest|bike]"), error);
+        assertTrue(error.contains("available: [fastest|car, fastest|bike]"), error);
+        assertLMProfileSelectionError(NO_MATCH_ERROR, lmProfiles, null, "shortest", null, null);
+
+        // edge_based/u_turn_costs is set, but lm should not really care
+        assertProfileFound(lmProfiles.get(0), lmProfiles, "car", null, null, null);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, "car", null, true, null);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, "car", null, null, 54);
+        assertProfileFound(lmProfiles.get(0), lmProfiles, "car", null, false, 64);
+    }
+
+
+    private void assertProfileFound(LMProfile expectedProfile, List<LMProfile> profiles, String vehicle, String weighting, Boolean edgeBased, Integer uTurnCosts) {
+        HintsMap hintsMap = createHintsMap(vehicle, weighting, edgeBased, uTurnCosts);
+        try {
+            LMProfile selectedProfile = new ProfileResolver().selectLMProfile(profiles, hintsMap);
+            assertEquals(expectedProfile, selectedProfile);
+        } catch (IllegalArgumentException e) {
+            fail("no profile found\nexpected: " + expectedProfile + "\nerror: " + e.getMessage());
+        }
+    }
+
+    private String assertLMProfileSelectionError(String expectedError, List<LMProfile> profiles, String vehicle, String weighting, Boolean edgeBased, Integer uTurnCosts) {
+        HintsMap hintsMap = createHintsMap(vehicle, weighting, edgeBased, uTurnCosts);
+        try {
+            new ProfileResolver().selectLMProfile(profiles, hintsMap);
+            fail("There should have been an error");
+            return "";
+        } catch (IllegalArgumentException e) {
+            assertTrue(e.getMessage().contains(expectedError),
+                    "There should have been an error message containing:\n'" + expectedError + "'\nbut was:\n'" + e.getMessage() + "'");
+            return e.getMessage();
+        }
+    }
+
+    private HintsMap createHintsMap(String vehicle, String weighting, Boolean edgeBased, Integer uTurnCosts) {
+        HintsMap hintsMap = new HintsMap().setWeighting(weighting).setVehicle(vehicle);
+        if (edgeBased != null) {
+            hintsMap.put(Parameters.Routing.EDGE_BASED, edgeBased);
+        }
+        if (uTurnCosts != null) {
+            hintsMap.put(Parameters.Routing.U_TURN_COSTS, uTurnCosts);
+        }
+        return hintsMap;
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupBuilderTest.java b/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupBuilderTest.java
index 38e17493a..28a81b809 100644
--- a/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupBuilderTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupBuilderTest.java
@@ -24,7 +24,6 @@ import com.graphhopper.routing.profiles.*;
 import com.graphhopper.routing.util.CarFlagEncoder;
 import com.graphhopper.routing.util.EncodingManager;
 import com.graphhopper.routing.util.parsers.SpatialRuleParser;
-import com.graphhopper.routing.util.spatialrules.SpatialRuleLookupBuilder.SpatialRuleFactory;
 import com.graphhopper.routing.util.spatialrules.countries.GermanySpatialRule;
 import com.graphhopper.storage.Graph;
 import com.graphhopper.storage.GraphBuilder;
@@ -41,8 +40,6 @@ import org.locationtech.jts.geom.Polygon;
 import java.io.FileReader;
 import java.io.IOException;
 import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
 
 import static com.graphhopper.util.GHUtility.updateDistancesFor;
 import static junit.framework.TestCase.assertFalse;
@@ -210,50 +207,4 @@ public class SpatialRuleLookupBuilderTest {
         e4.setFlags(em.handleWayTags(livingStreet2, map, relFlags));
         assertEquals(MaxSpeed.UNSET_SPEED, e4.get(tmpCarMaxSpeedEnc), .1);
     }
-
-    @Test
-    public void testSpeed() throws IOException {
-        final FileReader reader = new FileReader(COUNTRIES_FILE);
-        SpatialRuleFactory rulePerCountryFactory = new SpatialRuleFactory() {
-
-            @Override
-            public SpatialRule createSpatialRule(final String id, final List<Polygon> borders) {
-                return new AbstractSpatialRule(borders) {
-                    
-                    @Override
-                    public double getMaxSpeed(RoadClass roadClass, TransportationMode transport, double currentMaxSpeed) {
-                        return 100;
-                    }
-
-                    @Override
-                    public RoadAccess getAccess(RoadClass roadClass, TransportationMode transport, RoadAccess currentRoadAccess) {
-                        return RoadAccess.YES;
-                    }
-
-                    @Override
-                    public String getId() {
-                        return id;
-                    }
-                };
-            }
-        };
-        SpatialRuleLookup spatialRuleLookup = SpatialRuleLookupBuilder.buildIndex(Collections.singletonList(
-                Jackson.newObjectMapper().readValue(reader, JsonFeatureCollection.class)), "ISO_A3", rulePerCountryFactory);
-
-        // generate random points in central Europe
-        int randomPoints = 250_000;
-        long start = System.nanoTime();
-        for (int i = 0; i < randomPoints; i++) {
-            double lat = 46d + Math.random() * 7d;
-            double lon = 6d + Math.random() * 21d;
-            spatialRuleLookup.lookupRule(new GHPoint(lat, lon));
-        }
-
-        long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
-        // System.out.println("Lookup of " + randomPoints + " points took " + duration + "ms");
-        // System.out.println("Average lookup duration: " + ((double) duration) / randomPoints + "ms");
-        long maxBenchmarkRuntimeMs = 5_000L;
-        assertTrue("Benchmark must be finished in less than " + maxBenchmarkRuntimeMs + "ms",
-                duration < maxBenchmarkRuntimeMs);
-    }
 }
diff --git a/reader-osm/src/test/java/com/graphhopper/reader/osm/GraphHopperOSMTest.java b/reader-osm/src/test/java/com/graphhopper/reader/osm/GraphHopperOSMTest.java
index 475178b5e..31e525fa2 100644
--- a/reader-osm/src/test/java/com/graphhopper/reader/osm/GraphHopperOSMTest.java
+++ b/reader-osm/src/test/java/com/graphhopper/reader/osm/GraphHopperOSMTest.java
@@ -134,7 +134,7 @@ public class GraphHopperOSMTest {
                 setDataReaderFile(testOsm);
         gh.importOrLoad();
 
-        assertFalse(gh.getAlgorithmFactory(new HintsMap(weighting)) instanceof CHRoutingAlgorithmFactory);
+        assertFalse(gh.getAlgorithmFactory("profile", false, false) instanceof CHRoutingAlgorithmFactory);
 
         GHResponse rsp = gh.route(new GHRequest(51.2492152, 9.4317166, 51.2, 9.4)
                 .setVehicle(vehicle).setWeighting(weighting));
@@ -156,7 +156,7 @@ public class GraphHopperOSMTest {
                 setGraphHopperLocation(ghLoc).
                 setDataReaderFile(testOsm);
 
-        assertFalse(gh.getAlgorithmFactory(new HintsMap(weighting)) instanceof CHRoutingAlgorithmFactory);
+        assertFalse(gh.getAlgorithmFactory("profile", false, false) instanceof CHRoutingAlgorithmFactory);
         gh.close();
     }
 
@@ -988,23 +988,22 @@ public class GraphHopperOSMTest {
         CHPreparationHandler chHandler = new CHPreparationHandler();
         Weighting fwSimpleTruck = new FastestWeighting(simpleTruck);
         Weighting fwTruck = new FastestWeighting(truck);
-        GraphHopperStorage storage = new GraphBuilder(em).setCHProfiles(Arrays.asList(CHProfile.nodeBased(fwSimpleTruck), CHProfile.nodeBased(fwTruck))).build();
-        chHandler.addCHProfile(CHProfile.nodeBased(fwSimpleTruck));
-        chHandler.addCHProfile(CHProfile.nodeBased(fwTruck));
-        chHandler.addPreparation(PrepareContractionHierarchies.fromGraphHopperStorage(storage, CHProfile.nodeBased(fwSimpleTruck)));
-        chHandler.addPreparation(PrepareContractionHierarchies.fromGraphHopperStorage(storage, CHProfile.nodeBased(fwTruck)));
-
-        HintsMap wMap = new HintsMap("fastest");
-        wMap.put("vehicle", "truck");
-        assertEquals("fastest|truck", ((CHRoutingAlgorithmFactory) chHandler.getAlgorithmFactory(wMap)).getWeighting().toString());
-        wMap.put("vehicle", "simple_truck");
-        assertEquals("fastest|simple_truck", ((CHRoutingAlgorithmFactory) chHandler.getAlgorithmFactory(wMap)).getWeighting().toString());
+        CHProfile simpleTruckProfile = CHProfile.nodeBased("simple_truck", fwSimpleTruck);
+        CHProfile truckProfile = CHProfile.nodeBased("truck", fwTruck);
+        GraphHopperStorage storage = new GraphBuilder(em).setCHProfiles(Arrays.asList(simpleTruckProfile, truckProfile)).build();
+        chHandler.addCHProfile(simpleTruckProfile);
+        chHandler.addCHProfile(truckProfile);
+        chHandler.addPreparation(PrepareContractionHierarchies.fromGraphHopperStorage(storage, simpleTruckProfile));
+        chHandler.addPreparation(PrepareContractionHierarchies.fromGraphHopperStorage(storage, truckProfile));
+
+        assertEquals("fastest|truck", ((CHRoutingAlgorithmFactory) chHandler.getAlgorithmFactory("truck")).getWeighting().toString());
+        assertEquals("fastest|simple_truck", ((CHRoutingAlgorithmFactory) chHandler.getAlgorithmFactory("simple_truck")).getWeighting().toString());
 
         // make sure weighting cannot be mixed
-        chHandler.addCHProfile(CHProfile.nodeBased(fwTruck));
-        chHandler.addCHProfile(CHProfile.nodeBased(fwSimpleTruck));
+        chHandler.addCHProfile(truckProfile);
+        chHandler.addCHProfile(simpleTruckProfile);
         try {
-            chHandler.addPreparation(PrepareContractionHierarchies.fromGraphHopperStorage(storage, CHProfile.nodeBased(fwSimpleTruck)));
+            chHandler.addPreparation(PrepareContractionHierarchies.fromGraphHopperStorage(storage, simpleTruckProfile));
             fail();
         } catch (Exception ex) {
         }