Repository: graphhopper/graphhopper
Pull request #2209 (2020-12-10 09:36:50)

Java/Janino scripting to replace our interpreter
This change is intended to make custom profiles more powerful and also more readable. It breaks backward compatibility. It uses a very powerful and fast library called [Janino](https://janino-compiler.github.io/janino/) that allows on-the-fly compilation of partial expressions, blocks or whole classes written in Java.

With Janino scripting we can satisfy much more use cases. As soon as custom profiles were released there was often the need to combine expressions like "use factor=0.5 only if road_class is primary AND in a certain area" or "surface is unpaved AND road_class is not unclassified". This is now possible. Still I do not think we should use more power of Janino and keep the scripting simple (expressions only).

The big advantage with Janino scripting is now that we do not have the burden in the future to extend or improve our own interpreter. Performance-wise the routing with Janino scripting is at least of the same speed (for preparation it is even faster!). For query time this sounds a bit strange as the compilation takes some time. But when we assume that a custom profile is re-used for future requests of the same profile the class cache helps (see ExpressionBuilder, but the cache can be disabled). Now the most surprising thing for me was that even without the cache the slow down is not much (20% for LM queries? precise numbers needs to be re-done for the most recent commit). Janino is able to compile many expressions and a whole class in only a few milliseconds. And currently we create a cached version automatically for all server-side profiles due to GraphHopper.checkProfilesConsistency, i.e. for them there is no compilation overhead. btw: our cache is not that bad or complex as the cached object for key X is always the same (unlike for DBs with user data).

We have to add Janino as dependency to the core (1MB). I find this acceptable for what we get.

A custom profile in the current master looks like:

```yml
priority:
  max_height: {"<3.8": 0}

speed_factor:
  road_class:
    motorway: 0.85
    primary: 0.9

max_speed:
  road_class:
    living_street: 2

max_speed_fallback: 80
```

with this branch this gets more readable and you do not need to study the docs and still know what it does:

```yml
priority:
- if: max_height < 3.80
  multiply by: 0

speed:
- if: road_class == MOTORWAY: 
  multiply by: 0.85
- if: road_class == PRIMARY
  multiply by: 0.9
  # if you wish to keep compatibility you apply max_speed after all multiplications, but in general you can use "limit to" at any time
- if: road_class == LIVING_STREET
  limit to: 2
  # replacement for max_speed_fallback which is now implicit
  # it could be also done via the else statement of the last if ... or via:
- if: true
  limit to: 80
```

It is slightly more verbose for the enum case, but also more clear. 

And now new things like inequality `!=` or AND-expressions `&&` are possible and all other comparison operators (`<=`, `>`, ...). See this example:

```yml
priority:
- if: road_class == MOTORWAY && max_speed >= 70
  multiply by: 0.85
- if: road_class != PRIMARY
  multiply by: 0.9
```

We could even use (whitelisted) methods in the expression like `road.class.ordinal() < 3`.

The `multiply by` operation can now be `>1` for server-side profiles. Note that the custom profile in the **query** still has the limitation of `factor<=1`.

Also areas are still possible and can be combined with other expressions:

```yml
expressionA && in_area_custom1: 0.5

areas:
- custom1: <geojson>
```

Note that currently the area check is done for every edge before any expression is evaluated and so also if expressionA is false. I'm currently exploring options to avoid this.

For some use cases (and sometimes for performance) you can use else-if and else:

```yml
priority:
- if: <expressionA>
  multiply by: 0.85
- else if: <expressionB>
  multiply by: 0.6
- else: 
  multiply by: 0.8
```

Such a block is also what happens for a single encoded value in master branch so you can use this for migration.

---

Technically Janino compiles CustomWeightingHelper at runtime and adds user expressions to it before (in a safe manner). 

Most code changes and tests are necessary to make the janino compilation safe regarding attacks from user expressions. Of course it should not be possible to call System.exit, mine crypto currencies or read the file system on the routing servers that could be exposed to the public. Additionally to this work we could think about enabling the SecurityManager (see [here](https://github.com/graphhopper/graphhopper/tree/security_manager)) but this only further increases security and would not be the main protection. The main protection comes from the fact that we allow only "simple" expressions like "if" clauses and enforce that there are no assignments, no loops, no lambdas or other stuff happening in user expressions and have a whitelist of variable names and methods. We do this with the help of the Scanner API from Janino and traverse the code (ExpressionVisitor).

Also we want simple and readable user expressions. For that we need to declare and assign variables before like `road_class` and create the class variable `EncodedValue<RoadClass> roadClassEnc` and also make constants like `NO` more specific (convert it into `Toll.NO` if the `toll` encoded value is used). The user should not need to do that. With the nice API of Janino I found a safe way to mix arbitrary code with the user expressions. First I create Java objects (`List<Java.BlockStatement>`) from all the user expressions (it is ensured that they are simple) and inject them into the parsed class CustomWeightingHelper (`Java.CompilationUnit`) containing arbitrary Java code like assignments and the compile this into byte code, load the class and finally instantiate an object of it.

To my knowledge and experiments Janino does not use the Metaspace (unlike javaassist) and instead it creates the classes on the heap.

easbar: Performance-wise this looks really good. LM preparation is about the same and LM queries are even slightly faster or at least as fast as master.
michaz: Excellent!! Can I make a totally tangential remark about the YML-Schema? I find it a bit counter-intuitive that _expressions_ go on the left-hand-side of a key-value-pair. Similarly, is it right that the `_1` part of `first_match_1` gets parsed out and is interpreted as a number?

I realize this is technically allowed -- both sides are just strings. I just want to say that it may be perceived as surprising. In my mind at least, things that go on the left-hand-side of JSON objects are *either* things like field names (fixed in a kind of schema), *or* names that I, the user, give to things.

Would it make sense to refactor

```
first_match_1:
    road_environment == TUNNEL: 0.4
    some_other_expression: 0.3
first_match_2:
    surface == GRAVEL: 0.7
    some_other_expression_2: 0.3
```

to something like 

```
first_match:
   - expression: road_environment == TUNNEL
     value: 0.4
   - expression: surface == GRAVEL
     value: 0.7
```

Basically, use lists as lists and objects as objects where possible?

Functionality-wise, I'm totally hyped to try this out.

michaz: Or even:

```
priority:
   - type: expression 
     expression: max_height < 3.8
     value: 0
   - type: expression 
     expression: max_width < 2.5
     value: 0
   - type: first_match 
     expressions:
       - type: expression 
         expression: road_environment == TUNNEL
         value: 0.4
       - type: expression
         expression: some_other_expression
         value: 0.3
   - type: first_match
     expressions:
       - type: expression
         expression: surface == GRAVEL
         value: 0.7
       - type: expression 
         expression: some_other_expression_2
         value: 0.3
```

In that case, it would basically parse itself, because there's an explicit subtype selector (`type`), but maybe that goes too far....

easbar: And maybe rename `expression/value->if/factor` and `first_match/expression/value->switch/case/factor`: 

```yaml
priority:
   - if: max_height < 3.8
     factor: 0
   - if: max_width < 2.5
     factor: 0
   - switch:
       - case: road_environment == TUNNEL
         factor: 0.4
       - case: some_other_expression
         factor: 0.3
       - case: other/default/true(?)
         factor: 0.9
   - switch:
       - calse: surface == GRAVEL
         factor: 0.7
       - case: some_other_expression_2
         factor: 0.3
```

This is what we do, right? We use two directives: 'if condition then multiply with factor' and 'if condition then multiply with factor else if other_condition then multiply with other factor'?

easbar: And maybe this way we could also add an else clause to the simple if condition: 

```yaml
priority:
  - if: max_height < 3.8
    factor: 0
    else: 0.3
...
```


karussell: Yeah, we need to improve this. That the structure is a map and not a list was already the case in master and hit us there as a bug (#2162) and so I suggest to do this in a separate issue.

> And maybe rename expression/value->if/factor and first_match/expression/value->switch/case/factor:

I really like that. We could even avoid the separate handling of switch:

```yaml
priority:
   - if: max_height < 3.8
     factor: 0
   - if: max_width < 2.5
     factor: 0
   - if: road_environment == TUNNEL
     factor: 0.4
   - else if: some_other_expression
     factor: 0.3
   - else:
     factor: 0.9
```

My other suggestion would have been that we use a "computer-optimized" JSON format and a "human-optimized" format that is not necessarily a YAML but very easy to convert into the "computer-optimized" JSON format. We are already doing a similar separation in master as the YAML is only possible for the server-side profiles and in GH Maps (conversion happens on the client-side via JS).

The human format could evolve in future versions where the JSON API would not change. The human format could then be:

```
priority:
 *= 0.5 if road_class == PRIMARY
           else 0.9
 *= 0.3 if max_speed > 50
 *= 0.4 if road_environment == TUNNEL
           else if some_other_expression then 0.3
           else 0.9
```


The disadvantage is that no longer syntax highlighting or other tools help us. But the advantage is that we are not limited to the YAML rules like that whitespaces are significant. 
easbar: > We could even avoid the separate handling of switch:

Ah ok you mean the `else if` and `else` keys would always refer to the closest previous `if`? Yes this seems easier to understand than using `first_match` or switch, because it is just one construct one needs to understand: Apply the if/else if/else logic and multiply all factors that apply to an edge.

> The human format could evolve in future versions where the JSON API would not change. 

Yes this sounds interesting but so far I am not sure if we need this extra layer on top of the internal format, especially if the internal format is already pretty clean like your suggesting with the `if,else if,else` keys?
easbar: > it is just one construct one needs to understand: Apply the if/else if/else logic and multiply all factors that apply to an edge.

Except they are not always factors (I forgot). For `priority` and `speed` there are factors, but for `max_speed` there are (max) values.
karussell: > Ah ok you mean the else if and else keys would always refer to the closest previous if? 

Yes, as we have a defined order (list) it should be fine.

> because it is just one construct one needs to understand

:+1: 

Shall I try this here as it requires a refactoring of CustomModel too (?)

> The human format could evolve in future versions where the JSON API would not change. The human format could then be:

What I'm a bit surprised is that jackson & YAMLFactory is able to handle the values without quotation marks even for these string values. So I thought that instead:

```yaml
if: road_class == PRIMARY
```

we might need:

```yaml
if: "road_class == PRIMARY"
```

which would be ugly when StringEncodedValue or Strings are involved. But it seems to work :)
easbar: > which would be ugly when StringEncodedValue or Strings are involved. But it seems to work :)

So we just need to escape the inner quotes or would we not be able to do string comparison at all? 
karussell: > So we just need to escape the inner quotes or would we not be able to do string comparison at all?

Yes, for inner quotes. But again, for our jackson implementation it seems to work fine without quotes:

```
System.out.println(yamlToJson("if: max_speed > 0 || road_class == SECONDARY\ntest: true"));
```

From the spec ["7.3.3. Plain Style"](https://yaml.org/spec/1.2/spec.html) it looks we are fine, according to my limited understanding. They only say we cannot use `:` and `#` which makes sense and is fine for us.

But in general String stuff is very complicated in YAML (due to multiline support etc) and [SO says](https://stackoverflow.com/questions/19109912/yaml-do-i-need-quotes-for-strings-in-yaml) one should use quotes when using e.g. `&&` but we could also use single quotes `'` (works in jackson too)


easbar: Ok. To me it usually feels safer to put some more quotes everywhere, but if we can skip them this is also nice.
michaz: > To me it usually feels safer to put some more quotes everywhere, but if we can skip them this is also nice.

Yes!

Imagine that I, the user, know, or am happy to Google, how YML works and how Janino works. I don't want to put in much energy to find out how the GraphHopper guys got the Janino into the YML, or how they managed to get their notation short and save some dashes.

So the more "which symbols are parsed by YML and which by Janino" is abundantly clear, the better. So yes to quotes. :-)

karussell: > I don't want to put in much energy to find out how the GraphHopper guys got the Janino into the YML

My intention is to create a documentation so that the user does not have to do that or even needs to understand how we got Java expressions implemented. So no need to read something about our specific implementation or know Janino expressions as they are plain Java expressions. But we need to tell the user about our JSON format and how we combine the expressions to calculate the priority, speed_factor and max_speed and how this forms the final cost function. We currently do this in `profiles.md`.

> So the more "which symbols are parsed by YML and which by Janino" is abundantly clear, the better. So yes to quotes. :-)

Our main goal should be a stable JSON API (we only use JSON for the web API) and we always use quotes there, so this is fine I think :)

The YML part is something which can be changed much easier also later as we use this only for server-side profiles.

Patch:
diff --git a/README.md b/README.md
index 682bf34cb..a6623e2d8 100644
--- a/README.md
+++ b/README.md
@@ -213,7 +213,7 @@ Here is a list of the more detailed features:
  * [Alternative routes](https://discuss.graphhopper.com/t/alternative-routes/424)
  * [Turn costs and restrictions](../stable/docs/core/turn-restrictions.md)
  * Country specific routing via SpatialRules
- * The core uses only a few dependencies (hppc, jts and slf4j)
+ * The core uses only a few dependencies (hppc, jts, janino and slf4j)
  * 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))
  * Calculates isochrones and [shortest path trees](https://github.com/graphhopper/graphhopper/pull/1577)
diff --git a/api/src/main/java/com/graphhopper/json/Statement.java b/api/src/main/java/com/graphhopper/json/Statement.java
new file mode 100644
index 000000000..5e9b51037
--- /dev/null
+++ b/api/src/main/java/com/graphhopper/json/Statement.java
@@ -0,0 +1,104 @@
+/*
+ *  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.json;
+
+public class Statement {
+    private final Keyword keyword;
+    private final String expression;
+    private final Op operation;
+    private final double value;
+
+    private Statement(Keyword keyword, String expression, Op operation, double value) {
+        this.keyword = keyword;
+        this.expression = expression;
+        this.value = value;
+        this.operation = operation;
+    }
+
+    public Keyword getKeyword() {
+        return keyword;
+    }
+
+    public String getExpression() {
+        return expression;
+    }
+
+    public Op getOperation() {
+        return operation;
+    }
+
+    public double getValue() {
+        return value;
+    }
+
+    public enum Keyword {
+        IF("if"), ELSEIF("else if"), ELSE("else");
+
+        String name;
+
+        Keyword(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+
+    public enum Op {
+        MULTIPLY("multiply by"), LIMIT("limit to");
+
+        String name;
+
+        Op(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public String build(double value) {
+            switch (this) {
+                case MULTIPLY:
+                    return "value *= " + value;
+                case LIMIT:
+                    return "value = Math.min(value," + value + ")";
+                default:
+                    throw new IllegalArgumentException();
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        return keyword.getName() + ": " + expression + ", " + operation.getName() + ": " + value;
+    }
+
+    public static Statement If(String expression, Op op, double value) {
+        return new Statement(Keyword.IF, expression, op, value);
+    }
+
+    public static Statement ElseIf(String expression, Op op, double value) {
+        return new Statement(Keyword.ELSEIF, expression, op, value);
+    }
+
+    public static Statement Else(Op op, double value) {
+        return new Statement(Keyword.ELSE, null, op, value);
+    }
+}
diff --git a/api/src/main/java/com/graphhopper/util/CustomModel.java b/api/src/main/java/com/graphhopper/util/CustomModel.java
index 8e7d7ac58..d4f792d44 100644
--- a/api/src/main/java/com/graphhopper/util/CustomModel.java
+++ b/api/src/main/java/com/graphhopper/util/CustomModel.java
@@ -17,6 +17,8 @@
  */
 package com.graphhopper.util;
 
+import com.graphhopper.json.Statement;
+
 import java.util.*;
 
 /**
@@ -26,32 +28,43 @@ public class CustomModel {
 
     public static final String KEY = "custom_model";
 
-    static double DEFAULT_D_I = 70;
-    // optional:
-    private Double maxSpeedFallback;
-    private Double headingPenalty = Parameters.Routing.DEFAULT_HEADING_PENALTY;
-    // default value derived from the cost for time e.g. 25€/hour and for distance 0.5€/km, for trucks this is usually larger
-    private double distanceInfluence = DEFAULT_D_I;
-    private Map<String, Object> speedFactorMap = new LinkedHashMap<>();
-    private Map<String, Object> maxSpeedMap = new LinkedHashMap<>();
-    private Map<String, Object> priorityMap = new LinkedHashMap<>();
+    // e.g. 70 means that the time costs are 25€/hour and for the distance 0.5€/km (for trucks this is usually larger)
+    static double DEFAULT_DISTANCE_INFLUENCE = 70;
+    private Double distanceInfluence;
+    private double headingPenalty = Parameters.Routing.DEFAULT_HEADING_PENALTY;
+    private boolean internal;
+    private List<Statement> speedStatements = new ArrayList<>();
+    private List<Statement> priorityStatements = new ArrayList<>();
     private Map<String, JsonFeature> areas = new HashMap<>();
 
     public CustomModel() {
     }
 
     public CustomModel(CustomModel toCopy) {
-        this.maxSpeedFallback = toCopy.maxSpeedFallback;
         this.headingPenalty = toCopy.headingPenalty;
         this.distanceInfluence = toCopy.distanceInfluence;
+        // do not copy "internal"
 
-        speedFactorMap = deepCopy(toCopy.getSpeedFactor());
-        maxSpeedMap = deepCopy(toCopy.getMaxSpeed());
-        priorityMap = deepCopy(toCopy.getPriority());
+        speedStatements = deepCopy(toCopy.getSpeed());
+        priorityStatements = deepCopy(toCopy.getPriority());
 
         areas.putAll(toCopy.getAreas());
     }
 
+    /**
+     * This method is for internal usage only! Parsing a CustomModel is expensive and so we cache the result, which is
+     * especially important for fast landmark queries (hybrid mode). Now this method ensures that all server-side custom
+     * models are cached in a special internal cache which does not remove seldom accessed entries.
+     */
+    public CustomModel internal() {
+        this.internal = true;
+        return this;
+    }
+
+    public boolean isInternal() {
+        return internal;
+    }
+
     private <T> T deepCopy(T originalObject) {
         if (originalObject instanceof List) {
             List<Object> newList = new ArrayList<>(((List) originalObject).size());
@@ -72,25 +85,22 @@ public class CustomModel {
         }
     }
 
-    public Map<String, Object> getSpeedFactor() {
-        return speedFactorMap;
+    public List<Statement> getSpeed() {
+        return speedStatements;
     }
 
-    public Map<String, Object> getMaxSpeed() {
-        return maxSpeedMap;
-    }
-
-    public CustomModel setMaxSpeedFallback(Double maxSpeedFallback) {
-        this.maxSpeedFallback = maxSpeedFallback;
+    public CustomModel addToSpeed(Statement st) {
+        getSpeed().add(st);
         return this;
     }
 
-    public Double getMaxSpeedFallback() {
-        return maxSpeedFallback;
+    public List<Statement> getPriority() {
+        return priorityStatements;
     }
 
-    public Map<String, Object> getPriority() {
-        return priorityMap;
+    public CustomModel addToPriority(Statement st) {
+        getPriority().add(st);
+        return this;
     }
 
     public CustomModel setAreas(Map<String, JsonFeature> areas) {
@@ -108,7 +118,7 @@ public class CustomModel {
     }
 
     public double getDistanceInfluence() {
-        return distanceInfluence;
+        return distanceInfluence == null ? DEFAULT_DISTANCE_INFLUENCE : distanceInfluence;
     }
 
     public void setHeadingPenalty(double headingPenalty) {
@@ -126,43 +136,38 @@ public class CustomModel {
 
     private String createContentString() {
         // used to check against stored custom models, see #2026
-        return "distanceInfluence=" + distanceInfluence + "|speedFactor=" + speedFactorMap + "|maxSpeed=" + maxSpeedMap +
-                "|maxSpeedFallback=" + maxSpeedFallback + "|priorityMap=" + priorityMap + "|areas=" + areas;
+        return "distanceInfluence=" + distanceInfluence + "|headingPenalty=" + headingPenalty
+                + "|speedStatements=" + speedStatements + "|priorityStatements=" + priorityStatements + "|areas=" + areas;
     }
 
     /**
      * A new CustomModel is created from the baseModel merged with the specified queryModel.
      */
     public static CustomModel merge(CustomModel baseModel, CustomModel queryModel) {
-        // avoid changing the specified CustomModel via deep copy otherwise the server-side CustomModel would be modified (same problem if queryModel would be used as target)
+        if (queryModel.isInternal())
+            throw new IllegalArgumentException("CustomModel in query cannot be internal");
+
+        // avoid changing the specified CustomModel via deep copy otherwise the server-side CustomModel would be
+        // modified (same problem if queryModel would be used as target)
         CustomModel mergedCM = new CustomModel(baseModel);
-        if (queryModel.maxSpeedFallback != null) {
-            if (mergedCM.maxSpeedFallback != null && mergedCM.maxSpeedFallback > queryModel.maxSpeedFallback)
-                throw new IllegalArgumentException("CustomModel in query can only use max_speed_fallback bigger or equal to " + mergedCM.maxSpeedFallback);
-            mergedCM.maxSpeedFallback = queryModel.maxSpeedFallback;
-        }
-        if (Math.abs(queryModel.distanceInfluence - CustomModel.DEFAULT_D_I) > 0.01) {
-            if (mergedCM.distanceInfluence > queryModel.distanceInfluence)
-                throw new IllegalArgumentException("CustomModel in query can only use distance_influence bigger or equal to " + mergedCM.distanceInfluence);
+        // we only overwrite the distance influence if a non-default value was used
+        if (queryModel.distanceInfluence != null) {
+            if (queryModel.distanceInfluence < mergedCM.getDistanceInfluence())
+                throw new IllegalArgumentException("CustomModel in query can only use " +
+                        "distance_influence bigger or equal to " + mergedCM.getDistanceInfluence() +
+                        ", given: " + queryModel.distanceInfluence);
             mergedCM.distanceInfluence = queryModel.distanceInfluence;
         }
 
-        // example
-        // max_speed: { road_class: { secondary : 0.4 } }
-        // or
-        // priority:  { max_weight: { "<3.501": 0.7 } }
-        for (Map.Entry<String, Object> queryEntry : queryModel.getMaxSpeed().entrySet()) {
-            Object value = mergedCM.maxSpeedMap.get(queryEntry.getKey());
-            applyChange(mergedCM.maxSpeedMap, value, queryEntry);
-        }
-        for (Map.Entry<String, Object> queryEntry : queryModel.getSpeedFactor().entrySet()) {
-            Object value = mergedCM.speedFactorMap.get(queryEntry.getKey());
-            applyChange(mergedCM.speedFactorMap, value, queryEntry);
-        }
-        for (Map.Entry<String, Object> queryEntry : queryModel.getPriority().entrySet()) {
-            Object value = mergedCM.priorityMap.get(queryEntry.getKey());
-            applyChange(mergedCM.priorityMap, value, queryEntry);
-        }
+        checkFirst(queryModel.getSpeed());
+        checkFirst(queryModel.getPriority());
+
+        check(queryModel.getPriority());
+        check(queryModel.getSpeed());
+
+        mergedCM.speedStatements.addAll(queryModel.getSpeed());
+        mergedCM.priorityStatements.addAll(queryModel.getPriority());
+
         for (Map.Entry<String, JsonFeature> entry : queryModel.getAreas().entrySet()) {
             if (mergedCM.areas.containsKey(entry.getKey()))
                 throw new IllegalArgumentException("area " + entry.getKey() + " already exists");
@@ -172,92 +177,15 @@ public class CustomModel {
         return mergedCM;
     }
 
-    private static void applyChange(Map<String, Object> mergedSuperMap,
-                                    Object mergedObj, Map.Entry<String, Object> querySuperEntry) {
-        if (mergedObj == null) {
-            // no need for a merge
-            mergedSuperMap.put(querySuperEntry.getKey(), querySuperEntry.getValue());
-            return;
-        }
-        if (!(mergedObj instanceof Map))
-            throw new IllegalArgumentException(querySuperEntry.getKey() + ": entry is not a map: " + mergedObj);
-        Object queryObj = querySuperEntry.getValue();
-        if (!(queryObj instanceof Map))
-            throw new IllegalArgumentException(querySuperEntry.getKey() + ": query entry is not a map: " + queryObj);
-
-        Map<Object, Object> mergedMap = (Map) mergedObj;
-        Map<Object, Object> queryMap = (Map) queryObj;
-        for (Map.Entry queryEntry : queryMap.entrySet()) {
-            if (queryEntry.getKey() == null || queryEntry.getKey().toString().isEmpty())
-                throw new IllegalArgumentException(querySuperEntry.getKey() + ": key cannot be null or empty");
-            String key = queryEntry.getKey().toString();
-            if (isComparison(key))
-                continue;
-
-            Object mergedValue = mergedMap.get(key);
-            if (mergedValue == null) {
-                mergedMap.put(key, queryEntry.getValue());
-            } else if (multiply(queryEntry.getValue(), mergedValue) != null) {
-                // existing value needs to be multiplied
-                mergedMap.put(key, multiply(queryEntry.getValue(), mergedValue));
-            } else {
-                throw new IllegalArgumentException(querySuperEntry.getKey() + ": cannot merge value " + queryEntry.getValue() + " for key " + key + ", merged value: " + mergedValue);
-            }
-        }
-
-        // now special handling for comparison keys start e.g. <2 or >3.0, see testMergeComparisonKeys
-        // this could be simplified if CustomModel would be already an abstract syntax tree :)
-        List<String> queryComparisonKeys = getComparisonKeys(queryMap);
-        if (queryComparisonKeys.isEmpty())
-            return;
-        if (queryComparisonKeys.size() > 1)
-            throw new IllegalArgumentException(querySuperEntry.getKey() + ": entry in " + querySuperEntry.getValue() + " must not contain more than one key comparison but contained " + queryComparisonKeys);
-        char opChar = queryComparisonKeys.get(0).charAt(0);
-        List<String> mergedComparisonKeys = getComparisonKeys(mergedMap);
-        if (mergedComparisonKeys.isEmpty()) {
-            mergedMap.put(queryComparisonKeys.get(0), queryMap.get(queryComparisonKeys.get(0)));
-        } else if (mergedComparisonKeys.get(0).charAt(0) == opChar) {
-            if (multiply(queryMap.get(queryComparisonKeys.get(0)), mergedMap.get(mergedComparisonKeys.get(0))) != 0)
-                throw new IllegalArgumentException(querySuperEntry.getKey() + ": currently only blocking comparisons are allowed, but query was " + queryMap.get(queryComparisonKeys.get(0)) + " and server side: " + mergedMap.get(mergedComparisonKeys.get(0)));
-
-            try {
-                double comparisonMergedValue = Double.parseDouble(mergedComparisonKeys.get(0).substring(1));
-                double comparisonQueryValue = Double.parseDouble(queryComparisonKeys.get(0).substring(1));
-                if (opChar == '<') {
-                    if (comparisonMergedValue > comparisonQueryValue)
-                        throw new IllegalArgumentException(querySuperEntry.getKey() + ": only use a comparison key with a bigger value than " + comparisonMergedValue + " but was " + comparisonQueryValue);
-                } else if (opChar == '>') {
-                    if (comparisonMergedValue < comparisonQueryValue)
-                        throw new IllegalArgumentException(querySuperEntry.getKey() + ": only use a comparison key with a smaller value than " + comparisonMergedValue + " but was " + comparisonQueryValue);
-                } else {
-                    throw new IllegalArgumentException(querySuperEntry.getKey() + ": only use a comparison key with < or > as operator but was " + opChar);
-                }
-                mergedMap.remove(mergedComparisonKeys.get(0));
-                mergedMap.put(queryComparisonKeys.get(0), queryMap.get(queryComparisonKeys.get(0)));
-            } catch (NumberFormatException ex) {
-                throw new IllegalArgumentException(querySuperEntry.getKey() + ": number in one of the 'comparison' keys for " + querySuperEntry.getKey() + " wasn't parsable: " + queryComparisonKeys + " (" + mergedComparisonKeys + ")");
-            }
-        } else {
-            throw new IllegalArgumentException(querySuperEntry.getKey() + ": comparison keys must match but did not: " + queryComparisonKeys.get(0) + " vs " + mergedComparisonKeys.get(0));
-        }
-    }
-
-    static Double multiply(Object queryValue, Object mergedValue) {
-        if (queryValue instanceof Number && mergedValue instanceof Number)
-            return ((Number) queryValue).doubleValue() * ((Number) mergedValue).doubleValue();
-        return null;
-    }
-
-    static boolean isComparison(String key) {
-        return key.startsWith("<") || key.startsWith(">");
+    private static void checkFirst(List<Statement> priority) {
+        if (!priority.isEmpty() && priority.get(0).getKeyword() != Statement.Keyword.IF)
+            throw new IllegalArgumentException("First statement needs to be an if statement but was " + priority.get(0).getKeyword().getName());
     }
 
-    private static List<String> getComparisonKeys(Map<Object, Object> map) {
-        List<String> list = new ArrayList<>();
-        for (Map.Entry queryEntry : map.entrySet()) {
-            String key = queryEntry.getKey().toString();
-            if (isComparison(key)) list.add(key);
+    private static void check(List<Statement> list) {
+        for (Statement statement : list) {
+            if (statement.getOperation() == Statement.Op.MULTIPLY && statement.getValue() > 1)
+                throw new IllegalArgumentException("factor cannot be larger than 1 but was " + statement.getValue());
         }
-        return list;
     }
 }
\ No newline at end of file
diff --git a/api/src/main/java/com/graphhopper/util/Helper.java b/api/src/main/java/com/graphhopper/util/Helper.java
index 667bfc458..cc89fccad 100644
--- a/api/src/main/java/com/graphhopper/util/Helper.java
+++ b/api/src/main/java/com/graphhopper/util/Helper.java
@@ -63,10 +63,6 @@ public class Helper {
         return string.toUpperCase(Locale.ROOT);
     }
 
-    static String packageToPath(Package pkg) {
-        return pkg.getName().replaceAll("\\.", File.separator);
-    }
-
     public static int countBitValue(int maxTurnCosts) {
         if (maxTurnCosts < 0)
             throw new IllegalArgumentException("maxTurnCosts cannot be negative " + maxTurnCosts);
diff --git a/benchmark/little_custom.yml b/benchmark/little_custom.yml
index 7c21ef4e2..a4232c70f 100644
--- a/benchmark/little_custom.yml
+++ b/benchmark/little_custom.yml
@@ -1,8 +1,9 @@
 # this is a little customized model used to benchmark routing with a customized weighting that is similar to the
 # standard car profile
 priority:
-  road_access:
-    private: 0.1
-    destination: 0.1
+  - if: road_access == PRIVATE
+    multiply by: 0.1
+  - else if: road_access == DESTINATION
+    multiply by: 0.1
 
 distance_influence: 0
diff --git a/benchmark/very_custom.yml b/benchmark/very_custom.yml
index 2bc6f1a18..b8d56562f 100644
--- a/benchmark/very_custom.yml
+++ b/benchmark/very_custom.yml
@@ -1,20 +1,24 @@
 # this is a heavily customized model used to benchmark routing with a custom weighting
-speed_factor:
-  road_class:
-    motorway: 0.85
-    primary: 0.9
+speed:
+ - if: road_class == MOTORWAY
+   multiply by: 0.85
+ - else if: road_class == PRIMARY
+   multiply by: 0.9
+ - if: true
+   limit to: 110
 
 priority:
-  max_height: {"<3.8": 0}
-  max_width: {"<2.5": 0}
-  road_class:
-    motorway: 1
-    primary: 0.5
-    "*": 0.9
-  toll:
-    no: 1
-    "*": 0.5
-  hazmat:
-    no: 0
+  - if: max_height < 3.8
+    multiply by: 0
+  - if: max_width < 2.5
+    multiply by: 0
 
-max_speed_fallback: 110
\ No newline at end of file
+  - if: road_class == PRIMARY
+    multiply by: 0.5
+  - else if: road_class != MOTORWAY
+    multiply by: 0.9
+
+  - if: toll != NO
+    multiply by: 0.5
+  - if: hazmat == NO
+    multiply by: 0
diff --git a/core/files/changelog.txt b/core/files/changelog.txt
index 21c3400b3..f4aca8107 100644
--- a/core/files/changelog.txt
+++ b/core/files/changelog.txt
@@ -1,4 +1,5 @@
 3.0
+    CustomWeighting language breaks old format but is more powerful and easier to read; it also allows factors >1 for server-side CustomModels
 	renamed GHUtilities.setProperties to setSpeed
     the name of an encoded value can only contain lower letters, underscore or numbers. It has to start with a lower letter
     default for GraphHopperMatrixWeb (client for Matrix API) is now the sync POST request without the artificial polling delay in most cases
diff --git a/core/pom.xml b/core/pom.xml
index 97191944d..4b71d7307 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -44,6 +44,11 @@
             <groupId>com.carrotsearch</groupId>
             <artifactId>hppc</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.codehaus.janino</groupId>
+            <artifactId>janino</artifactId>
+            <version>3.1.2</version>
+        </dependency>
         <!-- for using CGIAR: elevation data importing via tif files-->
         <dependency>
             <groupId>org.apache.xmlgraphics</groupId>
diff --git a/core/src/main/java/com/graphhopper/routing/DefaultWeightingFactory.java b/core/src/main/java/com/graphhopper/routing/DefaultWeightingFactory.java
index ae3ab8495..1a19721ab 100644
--- a/core/src/main/java/com/graphhopper/routing/DefaultWeightingFactory.java
+++ b/core/src/main/java/com/graphhopper/routing/DefaultWeightingFactory.java
@@ -19,13 +19,14 @@
 package com.graphhopper.routing;
 
 import com.graphhopper.config.Profile;
-import com.graphhopper.util.CustomModel;
 import com.graphhopper.routing.util.EncodingManager;
 import com.graphhopper.routing.util.FlagEncoder;
 import com.graphhopper.routing.weighting.*;
+import com.graphhopper.routing.weighting.custom.CustomModelParser;
 import com.graphhopper.routing.weighting.custom.CustomProfile;
 import com.graphhopper.routing.weighting.custom.CustomWeighting;
 import com.graphhopper.storage.GraphHopperStorage;
+import com.graphhopper.util.CustomModel;
 import com.graphhopper.util.PMap;
 import com.graphhopper.util.Parameters;
 
@@ -76,7 +77,7 @@ public class DefaultWeightingFactory implements WeightingFactory {
             CustomProfile customProfile = (CustomProfile) profile;
             queryCustomModel = queryCustomModel == null ?
                     customProfile.getCustomModel() : CustomModel.merge(customProfile.getCustomModel(), queryCustomModel);
-            weighting = new CustomWeighting(encoder, encodingManager, turnCostProvider, queryCustomModel);
+            weighting = CustomModelParser.createWeighting(encoder, encodingManager, turnCostProvider, queryCustomModel);
         } else if ("shortest".equalsIgnoreCase(weightingStr)) {
             weighting = new ShortestWeighting(encoder, turnCostProvider);
         } else if ("fastest".equalsIgnoreCase(weightingStr)) {
diff --git a/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java b/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java
index b1a7cfbcf..d5319d157 100644
--- a/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java
+++ b/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java
@@ -475,6 +475,9 @@ public class EncodingManager implements EncodedValueLookup {
     private void addEncodedValue(EncodedValue ev, boolean withNamespace) {
         if (hasEncodedValue(ev.getName()))
             throw new IllegalStateException("EncodedValue " + ev.getName() + " already exists " + encodedValueMap.get(ev.getName()) + " vs " + ev);
+        String normalizedKey = ev.getName().replaceAll(SPECIAL_SEPARATOR, "_");
+        if (hasEncodedValue(normalizedKey))
+            throw new IllegalStateException("EncodedValue " + ev.getName() + " collides with " + normalizedKey);
         if (!withNamespace && !isSharedEncodedValues(ev))
             throw new IllegalArgumentException("EncodedValue " + ev.getName() + " must not contain namespace character '" + SPECIAL_SEPARATOR + "'");
         if (withNamespace && isSharedEncodedValues(ev))
@@ -793,7 +796,7 @@ public class EncodingManager implements EncodedValueLookup {
 
     private static final String SPECIAL_SEPARATOR = "$";
 
-    public static boolean isSharedEncodedValues(EncodedValue ev) {
+    private static boolean isSharedEncodedValues(EncodedValue ev) {
         return isValidEncodedValue(ev.getName()) && !ev.getName().contains(SPECIAL_SEPARATOR);
     }
 
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/BooleanToValueEntry.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/BooleanToValueEntry.java
deleted file mode 100644
index bcca3e470..000000000
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/BooleanToValueEntry.java
+++ /dev/null
@@ -1,85 +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.weighting.custom;
-
-import com.graphhopper.routing.ev.BooleanEncodedValue;
-import com.graphhopper.util.EdgeIteratorState;
-
-import java.util.Map;
-
-import static com.graphhopper.routing.weighting.custom.CustomWeighting.CATCH_ALL;
-import static com.graphhopper.routing.weighting.custom.EnumToValueEntry.getReturnValue;
-
-final class BooleanToValueEntry implements EdgeToValueEntry {
-    private final BooleanEncodedValue bev;
-    private final double value, elseValue;
-
-    private BooleanToValueEntry(BooleanEncodedValue bev, double value, double elseValue) {
-        this.bev = bev;
-        this.value = value;
-        this.elseValue = elseValue;
-    }
-
-    /**
-     * Example map:
-     * <pre>
-     * get_off_bike:
-     *   true: 0.4
-     *   false: 0.9 # optional and default is 1, equivalent to "*": 0.9
-     * </pre>
-     */
-    static EdgeToValueEntry create(String name, BooleanEncodedValue encodedValue, Map<String, Object> map,
-                                   double defaultValue, double minValue, double maxValue) {
-        if (map.isEmpty())
-            throw new IllegalArgumentException("Empty map for " + name);
-
-        // the key can only be a String, i.e. "false" and not a boolean false -> this is properly done from jackson
-        if (map.containsKey(CATCH_ALL) && map.containsKey("false"))
-            throw new IllegalArgumentException(name + ": cannot contain false and catch-all key at the same time");
-
-        double trueValue = Double.NaN;
-        double falseValue = defaultValue;
-        for (Map.Entry<String, Object> encValEntry : map.entrySet()) {
-            if (encValEntry.getKey() == null)
-                throw new IllegalArgumentException("key for " + name + " cannot be null, value: " + encValEntry.getValue());
-            String key = encValEntry.getKey();
-
-            double returnValue = getReturnValue(name, key, encValEntry.getValue(), minValue, maxValue);
-            if ("true".equals(key)) {
-                trueValue = returnValue;
-            } else if ("false".equals(key) || CATCH_ALL.equals(key)) {
-                falseValue = returnValue;
-            } else {
-                throw new IllegalArgumentException("key for " + name + " cannot be " + key + ", value: " + encValEntry.getValue());
-            }
-        }
-
-        return new BooleanToValueEntry(encodedValue, trueValue, falseValue);
-    }
-
-    @Override
-    public double getValue(EdgeIteratorState iter, boolean reverse) {
-        if (Double.isNaN(value)) return elseValue; // special case if only catch-all key is present
-        return (reverse ? iter.getReverse(bev) : iter.get(bev)) ? value : elseValue;
-    }
-
-    @Override
-    public String toString() {
-        return bev.getName() + ": " + value + ", else:" + elseValue;
-    }
-}
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomModelParser.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomModelParser.java
new file mode 100644
index 000000000..85a6fc18b
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomModelParser.java
@@ -0,0 +1,434 @@
+/*
+ *  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.weighting.custom;
+
+import com.graphhopper.json.Statement;
+import com.graphhopper.routing.ev.*;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.routing.util.FlagEncoder;
+import com.graphhopper.routing.weighting.TurnCostProvider;
+import com.graphhopper.util.CustomModel;
+import com.graphhopper.util.EdgeIteratorState;
+import com.graphhopper.util.GHUtility;
+import com.graphhopper.util.JsonFeature;
+import com.graphhopper.util.shapes.BBox;
+import com.graphhopper.util.shapes.Polygon;
+import org.codehaus.commons.compiler.CompileException;
+import org.codehaus.commons.compiler.Location;
+import org.codehaus.commons.compiler.io.Readers;
+import org.codehaus.janino.Scanner;
+import org.codehaus.janino.*;
+import org.codehaus.janino.util.DeepCopier;
+import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class CustomModelParser {
+    private static final AtomicLong longVal = new AtomicLong(1);
+    static final String IN_AREA_PREFIX = "in_area_";
+    private static final Set<String> allowedNames = new HashSet<>(Arrays.asList("edge", "Math"));
+    private static final boolean JANINO_DEBUG = Boolean.getBoolean(Scanner.SYSTEM_PROPERTY_SOURCE_DEBUGGING_ENABLE);
+
+    // Without a cache the class creation takes 10-40ms which makes routingLM8 requests 20% slower on average.
+    // CH requests and preparation is unaffected as cached weighting from preparation is used.
+    // Use accessOrder==true to remove oldest accessed entry, not oldest inserted.
+    private static final int CACHE_SIZE = Integer.getInteger("graphhopper.custom_weighting.cache_size", 1000);
+    private static final Map<String, Class<?>> CACHE = Collections.synchronizedMap(
+            new LinkedHashMap<String, Class<?>>(CACHE_SIZE, 0.75f, true) {
+                protected boolean removeEldestEntry(Map.Entry eldest) {
+                    return size() > CACHE_SIZE;
+                }
+            });
+
+    // This internal cache ensures that the "internal" Weighting classes specified in the profiles, are never removed regardless
+    // of how frequent other Weightings are created and accessed. We only need to synchronize the get and put methods alone.
+    // E.g. we do not care for the race condition where two identical classes are requested and one of them is overwritten.
+    // TODO perf compare with ConcurrentHashMap, but I guess, if there is a difference at all, it is not big for small maps
+    private static final Map<String, Class<?>> INTERNAL_CACHE = Collections.synchronizedMap(new HashMap<>());
+
+    private CustomModelParser() {
+        // utility class
+    }
+
+    public static CustomWeighting createWeighting(FlagEncoder baseFlagEncoder, EncodedValueLookup lookup, TurnCostProvider turnCostProvider,
+                                                  CustomModel customModel) {
+        if (customModel == null)
+            throw new IllegalStateException("CustomModel cannot be null");
+        CustomWeighting.Parameters parameters = createWeightingParameters(customModel, lookup, baseFlagEncoder.getMaxSpeed(), baseFlagEncoder.getAverageSpeedEnc());
+        return new CustomWeighting(baseFlagEncoder, turnCostProvider, parameters);
+    }
+
+    /**
+     * This method compiles a new subclass of CustomWeightingHelper composed from the provided CustomModel caches this
+     * and returns an instance.
+     */
+    static CustomWeighting.Parameters createWeightingParameters(CustomModel customModel, EncodedValueLookup lookup, double globalMaxSpeed,
+                                                                DecimalEncodedValue avgSpeedEnc) {
+        String key = customModel.toString() + ",global:" + globalMaxSpeed;
+        if (key.length() > 100_000) throw new IllegalArgumentException("Custom Model too big: " + key.length());
+
+        Class<?> clazz = customModel.isInternal() ? INTERNAL_CACHE.get(key) : null;
+        if (CACHE_SIZE > 0 && clazz == null)
+            clazz = CACHE.get(key);
+        if (clazz == null) {
+            clazz = createClazz(customModel, lookup, globalMaxSpeed);
+            if (customModel.isInternal()) {
+                INTERNAL_CACHE.put(key, clazz);
+                if (INTERNAL_CACHE.size() > 100) {
+                    CACHE.putAll(INTERNAL_CACHE);
+                    INTERNAL_CACHE.clear();
+                    LoggerFactory.getLogger(CustomModelParser.class).warn("Internal cache must stay small but was "
+                            + INTERNAL_CACHE.size() + ". Cleared it. Misuse of CustomModel::__internal_cache?");
+                }
+            } else if (CACHE_SIZE > 0) {
+                CACHE.put(key, clazz);
+            }
+        }
+
+        try {
+            // The class does not need to be thread-safe as we create an instance per request
+            CustomWeightingHelper prio = (CustomWeightingHelper) clazz.getDeclaredConstructor().newInstance();
+            prio.init(lookup, avgSpeedEnc, customModel.getAreas());
+            return new CustomWeighting.Parameters(prio::getSpeed, prio::getPriority, findMaxSpeed(customModel, globalMaxSpeed),
+                    customModel.getDistanceInfluence(), customModel.getHeadingPenalty());
+        } catch (ReflectiveOperationException ex) {
+            throw new IllegalArgumentException("Cannot compile expression " + ex.getMessage(), ex);
+        }
+    }
+
+    private static Class<?> createClazz(CustomModel customModel, EncodedValueLookup lookup, double globalMaxSpeed) {
+        try {
+            HashSet<String> priorityVariables = new LinkedHashSet<>();
+            List<Java.BlockStatement> priorityStatements = createGetPriorityStatements(priorityVariables, customModel, lookup);
+            HashSet<String> speedVariables = new LinkedHashSet<>();
+            List<Java.BlockStatement> speedStatements = createGetSpeedStatements(speedVariables, customModel, lookup, globalMaxSpeed);
+            // Create different class name, which is required only for debugging.
+            // TODO does it improve performance too? I.e. it could be that the JIT is confused if different classes
+            //  have the same name and it mixes performance stats. See https://github.com/janino-compiler/janino/issues/137
+            long counter = longVal.incrementAndGet();
+            String classTemplate = createClassTemplate(counter, priorityVariables, speedVariables, lookup, customModel);
+            Java.CompilationUnit cu = (Java.CompilationUnit) new Parser(new Scanner("source", new StringReader(classTemplate))).
+                    parseAbstractCompilationUnit();
+            cu = injectStatements(priorityStatements, speedStatements, cu);
+            SimpleCompiler sc = createCompiler(counter, cu);
+            return sc.getClassLoader().loadClass("com.graphhopper.routing.weighting.custom.JaninoCustomWeightingHelperSubclass" + counter);
+        } catch (Exception ex) {
+            String errString = "Cannot compile expression";
+            if (ex instanceof CompileException)
+                errString += ", in " + ((CompileException) ex).getLocation().getFileName();
+            throw new IllegalArgumentException(errString + " " + ex.getMessage(), ex);
+        }
+    }
+
+    /**
+     * Parse the expressions from CustomModel relevant for the method getSpeed - see createClassTemplate.
+     *
+     * @return the created statements (parsed expressions)
+     */
+    private static List<Java.BlockStatement> createGetSpeedStatements(Set<String> speedVariables,
+                                                                      CustomModel customModel, EncodedValueLookup lookup,
+                                                                      double globalMaxSpeed) throws Exception {
+        List<Java.BlockStatement> speedStatements = new ArrayList<>();
+        speedStatements.addAll(verifyExpressions(new StringBuilder(), "in 'speed' entry, ", speedVariables,
+                customModel.getSpeed(), lookup, "return Math.min(value, " + globalMaxSpeed + ");\n"));
+        String speedMethodStartBlock = "double value = super.getRawSpeed(edge, reverse);\n";
+        // a bit inefficient to possibly define variables twice, but for now we have two separate methods
+        for (String arg : speedVariables) {
+            speedMethodStartBlock += getVariableDeclaration(lookup, arg);
+        }
+        speedStatements.addAll(0, new Parser(new org.codehaus.janino.Scanner("getSpeed", new StringReader(speedMethodStartBlock))).
+                parseBlockStatements());
+        return speedStatements;
+    }
+
+    /**
+     * Parse the expressions from CustomModel relevant for the method getPriority - see createClassTemplate.
+     *
+     * @return the created statements (parsed expressions)
+     */
+    private static List<Java.BlockStatement> createGetPriorityStatements(Set<String> priorityVariables,
+                                                                         CustomModel customModel, EncodedValueLookup lookup) throws Exception {
+        List<Java.BlockStatement> priorityStatements = new ArrayList<>();
+        priorityStatements.addAll(verifyExpressions(new StringBuilder("double value = 1;\n"), "in 'priority' entry, ",
+                priorityVariables, customModel.getPriority(), lookup, "return value;"));
+        String priorityMethodStartBlock = "";
+        for (String arg : priorityVariables) {
+            priorityMethodStartBlock += getVariableDeclaration(lookup, arg);
+        }
+        priorityStatements.addAll(0, new Parser(new org.codehaus.janino.Scanner("getPriority", new StringReader(priorityMethodStartBlock))).
+                parseBlockStatements());
+        return priorityStatements;
+    }
+
+    static boolean isValidVariableName(String name) {
+        return name.startsWith(IN_AREA_PREFIX) || allowedNames.contains(name);
+    }
+
+    /**
+     * For the methods getSpeed and getPriority we declare variables that contain the encoded value of the current edge
+     * or if an area contains the current edge.
+     */
+    private static String getVariableDeclaration(EncodedValueLookup lookup, String arg) {
+        if (lookup.hasEncodedValue(arg)) {
+            EncodedValue enc = lookup.getEncodedValue(arg, EncodedValue.class);
+            return getReturnType(enc) + " " + arg + " = reverse ? " +
+                    "edge.getReverse((" + getInterface(enc) + ") this." + arg + "_enc) : " +
+                    "edge.get((" + getInterface(enc) + ") this." + arg + "_enc);\n";
+        } else if (isValidVariableName(arg)) {
+            return "";
+        } else {
+            throw new IllegalArgumentException("Not supported " + arg);
+        }
+    }
+
+    /**
+     * @return the interface as string of the provided EncodedValue, e.g. IntEncodedValue (only interface) or
+     * BooleanEncodedValue (first interface). For StringEncodedValue we return IntEncodedValue to return the index
+     * instead of the String for faster comparison.
+     */
+    private static String getInterface(EncodedValue enc) {
+        if (enc instanceof StringEncodedValue) return IntEncodedValue.class.getSimpleName();
+        if (enc.getClass().getInterfaces().length == 0) return enc.getClass().getSimpleName();
+        return enc.getClass().getInterfaces()[0].getSimpleName();
+    }
+
+    private static String getReturnType(EncodedValue encodedValue) {
+        String name = encodedValue.getClass().getSimpleName();
+        if (name.contains("Enum")) return "Enum";
+        if (name.contains("String")) return "int"; // we use indexOf
+        if (name.contains("Decimal")) return "double";
+        if (name.contains("Int")) return "int";
+        if (name.contains("Boolean")) return "boolean";
+        throw new IllegalArgumentException("Unsupported EncodedValue " + name);
+    }
+
+    /**
+     * Create the class source file from the detected variables (priorityVariables and speedVariables). We assume that
+     * these variables are safe although they are user input because we collected them from parsing via Janino. This
+     * means that the source file is free from user input and could be directly compiled. Before we do this we still
+     * have to inject that parsed and safe user expressions in a later step.
+     */
+    private static String createClassTemplate(long counter, Set<String> priorityVariables, Set<String> speedVariables,
+                                              EncodedValueLookup lookup, CustomModel customModel) {
+        final StringBuilder importSourceCode = new StringBuilder("import com.graphhopper.routing.ev.*;\n");
+        importSourceCode.append("import java.util.Map;\n");
+        final StringBuilder classSourceCode = new StringBuilder(100);
+        boolean includedAreaImports = false;
+
+        final StringBuilder initSourceCode = new StringBuilder("this.avg_speed_enc = avgSpeedEnc;\n");
+        Set<String> set = new HashSet<>(priorityVariables);
+        set.addAll(speedVariables);
+        for (String arg : set) {
+            if (lookup.hasEncodedValue(arg)) {
+                EncodedValue enc = lookup.getEncodedValue(arg, EncodedValue.class);
+                classSourceCode.append("protected " + enc.getClass().getSimpleName() + " " + arg + "_enc;\n");
+                initSourceCode.append("if (lookup.hasEncodedValue(\"" + arg + "\")) ");
+                initSourceCode.append("this." + arg + "_enc = (" + enc.getClass().getSimpleName()
+                        + ") lookup.getEncodedValue(\"" + arg + "\", EncodedValue.class);\n");
+            } else if (arg.startsWith(IN_AREA_PREFIX)) {
+                if (!includedAreaImports) {
+                    importSourceCode.append("import " + BBox.class.getName() + ";\n");
+                    importSourceCode.append("import " + GHUtility.class.getName() + ";\n");
+                    importSourceCode.append("import " + PreparedGeometryFactory.class.getName() + ";\n");
+                    importSourceCode.append("import " + JsonFeature.class.getName() + ";\n");
+                    importSourceCode.append("import " + Polygon.class.getName() + ";\n");
+                    includedAreaImports = true;
+                }
+
+                String id = arg.substring(IN_AREA_PREFIX.length());
+                if (!EncodingManager.isValidEncodedValue(id))
+                    throw new IllegalArgumentException("Area has invalid name: " + arg);
+                if (!customModel.getAreas().containsKey(id))
+                    throw new IllegalArgumentException("Area '" + id + "' wasn't found");
+                classSourceCode.append("protected " + Polygon.class.getSimpleName() + " " + arg + ";\n");
+                initSourceCode.append("JsonFeature feature = (JsonFeature) areas.get(\"" + id + "\");\n");
+                initSourceCode.append("this." + arg + " = new Polygon(new PreparedGeometryFactory().create(feature.getGeometry()));\n");
+            } else {
+                if (!isValidVariableName(arg))
+                    throw new IllegalArgumentException("Variable not supported: " + arg);
+            }
+        }
+
+        return ""
+                + "package com.graphhopper.routing.weighting.custom;"
+                + "import " + CustomWeightingHelper.class.getName() + ";\n"
+                + "import " + EncodedValueLookup.class.getName() + ";\n"
+                + "import " + EdgeIteratorState.class.getName() + ";\n"
+                + importSourceCode
+                + "\npublic class JaninoCustomWeightingHelperSubclass" + counter + " extends " + CustomWeightingHelper.class.getSimpleName() + " {\n"
+                + classSourceCode
+                + "   @Override\n"
+                + "   public void init(EncodedValueLookup lookup, "
+                + DecimalEncodedValue.class.getName() + " avgSpeedEnc, Map<String, " + JsonFeature.class.getName() + "> areas) {\n"
+                + initSourceCode
+                + "   }\n\n"
+                // we need these placeholder methods so that the hooks in DeepCopier are invoked
+                + "   @Override\n"
+                + "   public double getPriority(EdgeIteratorState edge, boolean reverse) {\n"
+                + "      return 1; //will be overwritten by code injected in DeepCopier\n"
+                + "   }\n"
+                + "   @Override\n"
+                + "   public double getSpeed(EdgeIteratorState edge, boolean reverse) {\n"
+                + "      return getRawSpeed(edge, reverse); //will be overwritten by code injected in DeepCopier\n"
+                + "   }\n"
+                + "}";
+    }
+
+    /**
+     * This method does:
+     * 1. check user expressions via Parser.parseConditionalExpression and only allow whitelisted variables and methods.
+     * 2. while this check it also guesses the variable names and stores it in createObjects
+     * 3. creates if-then-elseif expressions from the checks and returns them as BlockStatements
+     *
+     * @return the created if-then, else and elseif statements
+     */
+    private static List<Java.BlockStatement> verifyExpressions(StringBuilder expressions, String info, Set<String> createObjects,
+                                                               List<Statement> list, EncodedValueLookup lookup,
+                                                               String lastStmt) throws Exception {
+        // allow variables, all encoded values, constants
+        ExpressionVisitor.NameValidator nameInConditionValidator = name -> lookup.hasEncodedValue(name)
+                || name.toUpperCase(Locale.ROOT).equals(name) || isValidVariableName(name);
+        ExpressionVisitor.parseExpressions(expressions, nameInConditionValidator, info, createObjects, list, lookup, lastStmt);
+        return new Parser(new org.codehaus.janino.Scanner(info, new StringReader(expressions.toString()))).
+                parseBlockStatements();
+    }
+
+    /**
+     * Injects the already parsed expressions (converted to BlockStatement) via janinos DeepCopier to the provided
+     * CompilationUnit cu (a class file).
+     */
+    private static Java.CompilationUnit injectStatements(List<Java.BlockStatement> priorityStatements,
+                                                         List<Java.BlockStatement> speedStatements,
+                                                         Java.CompilationUnit cu) throws CompileException {
+        cu = new DeepCopier() {
+            boolean speedInjected = false;
+            boolean priorityInjected = false;
+
+            @Override
+            public Java.FieldDeclaration copyFieldDeclaration(Java.FieldDeclaration subject) throws CompileException {
+                // for https://github.com/janino-compiler/janino/issues/135
+                Java.FieldDeclaration fd = super.copyFieldDeclaration(subject);
+                fd.setEnclosingScope(subject.getEnclosingScope());
+                return fd;
+            }
+
+            @Override
+            public Java.MethodDeclarator copyMethodDeclarator(Java.MethodDeclarator subject) throws CompileException {
+                if (subject.name.equals("getSpeed") && !speedStatements.isEmpty() && !speedInjected) {
+                    speedInjected = true;
+                    return injectStatements(subject, this, speedStatements);
+                } else if (subject.name.equals("getPriority") && !priorityStatements.isEmpty() && !priorityInjected) {
+                    priorityInjected = true;
+                    return injectStatements(subject, this, priorityStatements);
+                } else {
+                    return super.copyMethodDeclarator(subject);
+                }
+            }
+        }.copyCompilationUnit(cu);
+        return cu;
+    }
+
+    private static Java.MethodDeclarator injectStatements(Java.MethodDeclarator subject, DeepCopier deepCopier,
+                                                          List<Java.BlockStatement> statements) {
+        try {
+            if (statements.isEmpty())
+                throw new IllegalArgumentException("Statements cannot be empty when copying method");
+            Java.MethodDeclarator methodDecl = new Java.MethodDeclarator(
+                    new Location("m1", 1, 1),
+                    subject.getDocComment(),
+                    deepCopier.copyModifiers(subject.getModifiers()),
+                    deepCopier.copyOptionalTypeParameters(subject.typeParameters),
+                    deepCopier.copyType(subject.type),
+                    subject.name,
+                    deepCopier.copyFormalParameters(subject.formalParameters),
+                    deepCopier.copyTypes(subject.thrownExceptions),
+                    deepCopier.copyOptionalElementValue(subject.defaultValue),
+                    deepCopier.copyOptionalStatements(statements)
+            );
+            statements.forEach(st -> st.setEnclosingScope(methodDecl));
+            return methodDecl;
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private static SimpleCompiler createCompiler(long counter, Java.AbstractCompilationUnit cu) throws CompileException {
+        if (JANINO_DEBUG) {
+            try {
+                StringWriter sw = new StringWriter();
+                Unparser.unparse(cu, sw);
+                // System.out.println(sw.toString());
+                File dir = new File("./src/main/java/com/graphhopper/routing/weighting/custom");
+                File temporaryFile = new File(dir, "JaninoCustomWeightingHelperSubclass" + counter + ".java");
+                Reader reader = Readers.teeReader(
+                        new StringReader(sw.toString()), // in
+                        new FileWriter(temporaryFile),   // out
+                        true               // closeWriterOnEoi
+                );
+                return new SimpleCompiler(temporaryFile.getAbsolutePath(), reader);
+            } catch (Exception ex) {
+                throw new RuntimeException(ex);
+            }
+        } else {
+            SimpleCompiler compiler = new SimpleCompiler();
+            // compiler.setWarningHandler((handle, message, location) -> System.out.println(handle + ", " + message + ", " + location));
+            compiler.cook(cu);
+            return compiler;
+        }
+    }
+
+    static double findMaxSpeed(CustomModel customModel, final double maxSpeed) {
+        double globalMin_maxSpeed = maxSpeed;
+        double blockMax_maxSpeed = 0;
+        for (Statement statement : customModel.getSpeed()) {
+            // Lowering the max_speed estimate for 'limit to' only (TODO later also for MULTIPLY)
+            if (statement.getOperation() == Statement.Op.LIMIT) {
+                if (statement.getValue() > maxSpeed)
+                    throw new IllegalArgumentException("Can never apply 'limit to': " + statement.getValue()
+                            + " because maximum vehicle speed is " + maxSpeed);
+
+                switch (statement.getKeyword()) {
+                    case IF:
+                        if ("true".equals(statement.getExpression())) {
+                            blockMax_maxSpeed = globalMin_maxSpeed = Math.min(statement.getValue(), maxSpeed);
+                        } else {
+                            blockMax_maxSpeed = statement.getValue();
+                        }
+                        break;
+                    case ELSEIF:
+                        blockMax_maxSpeed = Math.max(blockMax_maxSpeed, statement.getValue());
+                        break;
+                    case ELSE:
+                        blockMax_maxSpeed = Math.max(blockMax_maxSpeed, statement.getValue());
+                        globalMin_maxSpeed = Math.min(globalMin_maxSpeed, blockMax_maxSpeed);
+                        break;
+                    default:
+                        throw new IllegalArgumentException("unknown keyword " + statement.getKeyword());
+                }
+
+                if (globalMin_maxSpeed <= 0)
+                    throw new IllegalArgumentException("speed is always limited to 0. This must not be but results from " + statement);
+            }
+        }
+
+        return globalMin_maxSpeed;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomProfile.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomProfile.java
index 507fc6f78..687cc2344 100644
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomProfile.java
+++ b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomProfile.java
@@ -35,6 +35,7 @@ public class CustomProfile extends Profile {
     }
 
     public CustomProfile setCustomModel(CustomModel customModel) {
+        customModel.internal();
         getHints().putObject(CustomModel.KEY, customModel);
         getHints().putObject("custom_model_file", "empty");
         return this;
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomWeighting.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomWeighting.java
index e4e1e3a3b..fcbfaec6a 100644
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomWeighting.java
+++ b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomWeighting.java
@@ -18,7 +18,6 @@
 package com.graphhopper.routing.weighting.custom;
 
 import com.graphhopper.routing.ev.BooleanEncodedValue;
-import com.graphhopper.routing.ev.EncodedValueLookup;
 import com.graphhopper.routing.util.FlagEncoder;
 import com.graphhopper.routing.weighting.AbstractWeighting;
 import com.graphhopper.routing.weighting.TurnCostProvider;
@@ -68,14 +67,10 @@ import com.graphhopper.util.EdgeIteratorState;
  * The open parameters that we can adjust are therefore: speed_factor, priority and distance_influence and they are
  * specified via the `{@link CustomModel}`. The speed can also be restricted to a maximum value, in which case the value
  * calculated via the speed_factor is simply overwritten. Edges that are not accessible according to the access flags of
- * the base vehicle always get assigned an infinite weight and this cannot be changed using this weighting.
- *
- * @see SpeedCalculator for details how speed_factor and max_speed are derived from some given edge properties
- * @see PriorityCalculator for details how priority is derived for some given edge properties
+ * the base vehicle always get assigned an infinite weight and this cannot be changed (yet) using this weighting.
  */
 public final class CustomWeighting extends AbstractWeighting {
     public static final String NAME = "custom";
-    public static final String CATCH_ALL = "*";
 
     /**
      * Converting to seconds is not necessary but makes adding other penalties easier (e.g. turn
@@ -86,26 +81,21 @@ public final class CustomWeighting extends AbstractWeighting {
     private final double maxSpeed;
     private final double distanceInfluence;
     private final double headingPenaltySeconds;
-    private final SpeedCalculator speedCalculator;
-    private final PriorityCalculator priorityCalculator;
+    private final EdgeToDoubleMapping edgeToSpeedMapping;
+    private final EdgeToDoubleMapping edgeToPriorityMapping;
 
-    public CustomWeighting(FlagEncoder baseFlagEncoder, EncodedValueLookup lookup,
-                           TurnCostProvider turnCostProvider, CustomModel customModel) {
+    public CustomWeighting(FlagEncoder baseFlagEncoder, TurnCostProvider turnCostProvider, Parameters parameters) {
         super(baseFlagEncoder, turnCostProvider);
-        if (customModel == null)
-            throw new IllegalStateException("CustomModel cannot be null");
-
-        headingPenaltySeconds = customModel.getHeadingPenalty();
-        baseVehicleAccessEnc = baseFlagEncoder.getAccessEnc();
-        speedCalculator = new SpeedCalculator(baseFlagEncoder.getMaxSpeed(), customModel, baseFlagEncoder.getAverageSpeedEnc(), lookup);
-        maxSpeed = speedCalculator.getMaxSpeed() / SPEED_CONV;
-
-        priorityCalculator = new PriorityCalculator(customModel, lookup);
+        this.edgeToSpeedMapping = parameters.getEdgeToSpeedMapping();
+        this.edgeToPriorityMapping = parameters.getEdgeToPriorityMapping();
+        this.baseVehicleAccessEnc = baseFlagEncoder.getAccessEnc();
+        this.headingPenaltySeconds = parameters.getHeadingPenaltySeconds();
+        this.maxSpeed = parameters.getMaxSpeed() / SPEED_CONV;
 
         // given unit is s/km -> convert to s/m
-        distanceInfluence = customModel.getDistanceInfluence() / 1000;
-        if (distanceInfluence < 0)
-            throw new IllegalArgumentException("maximum distance_influence cannot be negative " + distanceInfluence);
+        this.distanceInfluence = parameters.getDistanceInfluence() / 1000.0;
+        if (this.distanceInfluence < 0)
+            throw new IllegalArgumentException("distance_influence cannot be negative " + this.distanceInfluence);
     }
 
     @Override
@@ -122,7 +112,7 @@ public final class CustomWeighting extends AbstractWeighting {
         double distanceCosts = distance * distanceInfluence;
         if (Double.isInfinite(distanceCosts))
             return Double.POSITIVE_INFINITY;
-        double priority = priorityCalculator.calcPriority(edgeState, reverse);
+        double priority = edgeToPriorityMapping.get(edgeState, reverse);
         return seconds / priority + distanceCosts;
     }
 
@@ -135,7 +125,8 @@ public final class CustomWeighting extends AbstractWeighting {
         if (reverse ? !edgeState.getReverse(baseVehicleAccessEnc) : !edgeState.get(baseVehicleAccessEnc))
             return Double.POSITIVE_INFINITY;
 
-        double speed = speedCalculator.calcSpeed(edgeState, reverse);
+        double speed = edgeToSpeedMapping.get(edgeState, reverse);
+        assert speed <= maxSpeed * SPEED_CONV : "for " + getName() + " speed <= maxSpeed is violated, " + speed + " <= " + maxSpeed * SPEED_CONV;
         if (speed == 0)
             return Double.POSITIVE_INFINITY;
         if (speed < 0)
@@ -155,4 +146,46 @@ public final class CustomWeighting extends AbstractWeighting {
     public String getName() {
         return NAME;
     }
+
+    @FunctionalInterface
+    public interface EdgeToDoubleMapping {
+        double get(EdgeIteratorState edge, boolean reverse);
+    }
+
+    static class Parameters {
+        private final EdgeToDoubleMapping edgeToSpeedMapping;
+        private final EdgeToDoubleMapping edgeToPriorityMapping;
+        private final double maxSpeed;
+        private final double distanceInfluence;
+        private final double headingPenaltySeconds;
+
+        Parameters(EdgeToDoubleMapping edgeToSpeedMapping, EdgeToDoubleMapping edgeToPriorityMapping,
+                   double maxSpeed, double distanceInfluence, double headingPenaltySeconds) {
+            this.edgeToSpeedMapping = edgeToSpeedMapping;
+            this.edgeToPriorityMapping = edgeToPriorityMapping;
+            this.maxSpeed = maxSpeed;
+            this.distanceInfluence = distanceInfluence;
+            this.headingPenaltySeconds = headingPenaltySeconds;
+        }
+
+        public EdgeToDoubleMapping getEdgeToSpeedMapping() {
+            return edgeToSpeedMapping;
+        }
+
+        public EdgeToDoubleMapping getEdgeToPriorityMapping() {
+            return edgeToPriorityMapping;
+        }
+
+        public double getMaxSpeed() {
+            return maxSpeed;
+        }
+
+        public double getDistanceInfluence() {
+            return distanceInfluence;
+        }
+
+        public double getHeadingPenaltySeconds() {
+            return headingPenaltySeconds;
+        }
+    }
 }
\ No newline at end of file
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomWeightingHelper.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomWeightingHelper.java
new file mode 100644
index 000000000..5657e2efb
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomWeightingHelper.java
@@ -0,0 +1,66 @@
+/*
+ *  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.weighting.custom;
+
+import com.graphhopper.routing.ev.DecimalEncodedValue;
+import com.graphhopper.routing.ev.EncodedValueLookup;
+import com.graphhopper.util.EdgeIteratorState;
+import com.graphhopper.util.FetchMode;
+import com.graphhopper.util.GHUtility;
+import com.graphhopper.util.JsonFeature;
+import com.graphhopper.util.shapes.BBox;
+import com.graphhopper.util.shapes.Polygon;
+
+import java.util.Map;
+
+/**
+ * This class is for internal usage only. It is subclassed by Janino, then special expressions are injected into init,
+ * getSpeed and getPriority. At the end an instance is created and used in CustomWeighting.
+ */
+public class CustomWeightingHelper {
+    protected DecimalEncodedValue avg_speed_enc;
+
+    protected CustomWeightingHelper() {
+    }
+
+    public void init(EncodedValueLookup lookup, DecimalEncodedValue avgSpeedEnc, Map<String, JsonFeature> areas) {
+        this.avg_speed_enc = avgSpeedEnc;
+    }
+
+    public double getPriority(EdgeIteratorState edge, boolean reverse) {
+        return 1;
+    }
+
+    public double getSpeed(EdgeIteratorState edge, boolean reverse) {
+        return getRawSpeed(edge, reverse);
+    }
+
+    protected final double getRawSpeed(EdgeIteratorState edge, boolean reverse) {
+        double speed = reverse ? edge.getReverse(avg_speed_enc) : edge.get(avg_speed_enc);
+        if (Double.isInfinite(speed) || Double.isNaN(speed) || speed < 0)
+            throw new IllegalStateException("Invalid estimated speed " + speed);
+        return speed;
+    }
+
+    public static boolean in(Polygon p, EdgeIteratorState edge) {
+        BBox bbox = GHUtility.createBBox(edge);
+        if (p.getBounds().intersects(bbox))
+            return p.intersects(edge.fetchWayGeometry(FetchMode.ALL).makeImmutable()); // TODO PERF: cache bbox and edge wayGeometry for multiple area
+        return false;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/DecimalToValueEntry.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/DecimalToValueEntry.java
deleted file mode 100644
index 85099b042..000000000
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/DecimalToValueEntry.java
+++ /dev/null
@@ -1,116 +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.weighting.custom;
-
-import com.graphhopper.routing.ev.DecimalEncodedValue;
-import com.graphhopper.util.EdgeIteratorState;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import static com.graphhopper.routing.weighting.custom.CustomWeighting.CATCH_ALL;
-import static com.graphhopper.routing.weighting.custom.EnumToValueEntry.getReturnValue;
-
-final class DecimalToValueEntry implements EdgeToValueEntry {
-    private final DecimalEncodedValue dev;
-    private final double minExclusive, maxExclusive;
-    private final double rangeValue;
-    private final double fallback;
-
-    private DecimalToValueEntry(DecimalEncodedValue dev, Range range, double fallback) {
-        this.dev = dev;
-        this.minExclusive = range.min;
-        this.maxExclusive = range.max;
-        this.rangeValue = range.value;
-        this.fallback = fallback;
-    }
-
-    @Override
-    public double getValue(EdgeIteratorState iter, boolean reverse) {
-        double edgeValue = reverse ? iter.getReverse(dev) : iter.get(dev);
-        return edgeValue < maxExclusive && edgeValue > minExclusive ? rangeValue : fallback;
-    }
-
-    static class Range {
-        final double min, max, value;
-
-        public Range(double min, double max, double value) {
-            this.min = min;
-            this.max = max;
-            this.value = value;
-            if (max < min)
-                throw new IllegalArgumentException("Wrong range. Minimum " + min + " cannot be bigger than maximum " + max);
-        }
-
-        @Override
-        public String toString() {
-            return "min=" + min + ", max=" + max + ", value=" + value;
-        }
-    }
-
-    static EdgeToValueEntry create(String name, DecimalEncodedValue dev, Map<String, Object> map,
-                                   double defaultValue, double minValue, double maxValue) {
-        if (map.isEmpty())
-            throw new IllegalArgumentException("Empty map for " + name);
-
-        Object evEntryValue = map.get(CATCH_ALL);
-        if (evEntryValue != null)
-            defaultValue = getReturnValue(name, CATCH_ALL, evEntryValue, minValue, maxValue);
-
-        List<Range> ranges = new ArrayList<>();
-        for (Map.Entry<String, Object> encValEntry : map.entrySet()) {
-            if (encValEntry.getKey() == null)
-                throw new IllegalArgumentException("key for " + name + " cannot be null, value: " + encValEntry.getValue());
-            String key = encValEntry.getKey();
-            if (CATCH_ALL.equals(key))
-                continue;
-
-            double returnValue = getReturnValue(name, key, encValEntry.getValue(), minValue, maxValue);
-            Range range = parseRange(name, key, returnValue);
-            ranges.add(range);
-        }
-        if (ranges.size() != 1)
-            throw new IllegalArgumentException("Currently only one range can be specified but was " + ranges.size());
-        return new DecimalToValueEntry(dev, ranges.get(0), defaultValue);
-    }
-
-    static Range parseRange(String name, String rangeAsString, Object value) {
-        if (!(value instanceof Number))
-            throw new IllegalArgumentException(name + " value has to be a number but was " + value);
-
-        double num = ((Number) value).doubleValue();
-        try {
-            int gtIndex = rangeAsString.indexOf(">");
-            if (gtIndex >= 0)
-                return new Range(Double.parseDouble(rangeAsString.substring(gtIndex + 1)), Double.POSITIVE_INFINITY, num);
-
-            int ltIndex = rangeAsString.indexOf("<");
-            if (ltIndex >= 0)
-                // unsigned => 0 is currently the minimum
-                return new Range(0, Double.parseDouble(rangeAsString.substring(ltIndex + 1)), num);
-        } catch (Exception ex) {
-        }
-        throw new IllegalArgumentException("Range is invalid. It must be e.g. \">3\" or \"<5\" but was: " + rangeAsString);
-    }
-
-    @Override
-    public String toString() {
-        return dev.getName() + ", range: min:" + minExclusive + ", max:" + maxExclusive + ", value:" + rangeValue;
-    }
-}
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/EdgeToValueEntry.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/EdgeToValueEntry.java
deleted file mode 100644
index a872b39a5..000000000
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/EdgeToValueEntry.java
+++ /dev/null
@@ -1,33 +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.weighting.custom;
-
-import com.graphhopper.routing.ev.EncodedValue;
-import com.graphhopper.util.EdgeIteratorState;
-
-/**
- * Implementations of this internal interface represent the conversion rules we use to map {@link EncodedValue}s to
- * numerical values when determining the different parameters for {@link CustomWeighting}.
- */
-interface EdgeToValueEntry {
-
-    /**
-     * @return the numeric value that shall be associated with the given (directed) edge
-     */
-    double getValue(EdgeIteratorState iter, boolean reverse);
-}
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/EnumToValueEntry.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/EnumToValueEntry.java
deleted file mode 100644
index 6362705f8..000000000
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/EnumToValueEntry.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.weighting.custom;
-
-import com.graphhopper.routing.ev.EnumEncodedValue;
-import com.graphhopper.routing.ev.IntEncodedValue;
-import com.graphhopper.util.EdgeIteratorState;
-
-import java.util.Arrays;
-import java.util.Map;
-
-import static com.graphhopper.routing.weighting.custom.CustomWeighting.CATCH_ALL;
-
-final class EnumToValueEntry implements EdgeToValueEntry {
-    private final IntEncodedValue eev;
-    private final double[] values;
-
-    private EnumToValueEntry(EnumEncodedValue eev, double[] values) {
-        this.eev = eev;
-        this.values = values;
-    }
-
-    @Override
-    public double getValue(EdgeIteratorState iter, boolean reverse) {
-        int enumOrdinal = reverse ? iter.getReverse(eev) : iter.get(eev);
-        return values[enumOrdinal];
-    }
-
-    /**
-     * Example map:
-     * <pre>
-     * road_class:
-     *   motorway: 0.4
-     *   "*": 0.9      # optional and default is 1
-     * </pre>
-     */
-    static EnumToValueEntry create(String name, EnumEncodedValue enumEncodedValue, Map<String, Object> map,
-                                   double defaultValue, double minValue, double maxValue) {
-        Enum[] enumValues = enumEncodedValue.getValues();
-        if (map.isEmpty())
-            throw new IllegalArgumentException("Empty map for " + name);
-
-        Object evEntryValue = map.get(CATCH_ALL);
-        if (evEntryValue != null)
-            defaultValue = getReturnValue(name, CATCH_ALL, evEntryValue, minValue, maxValue);
-
-        double[] tmp = new double[enumValues.length];
-        Arrays.fill(tmp, defaultValue);
-        for (Map.Entry<String, Object> encValEntry : map.entrySet()) {
-            if (encValEntry.getKey() == null)
-                throw new IllegalArgumentException("key for " + name + " cannot be null, value: " + encValEntry.getValue());
-            String key = encValEntry.getKey();
-            if (CATCH_ALL.equals(key))
-                continue;
-
-            Enum enumValue = getValueOf(enumValues, key);
-            double returnValue = getReturnValue(name, key, encValEntry.getValue(), minValue, maxValue);
-            tmp[enumValue.ordinal()] = returnValue;
-        }
-
-        return new EnumToValueEntry(enumEncodedValue, tmp);
-    }
-
-    static double getReturnValue(String name, String key, Object valueObject, double minValue, double maxValue) {
-        if (valueObject == null)
-            throw new IllegalArgumentException("value for " + name + " cannot be null, key: " + key);
-        if (!(valueObject instanceof Number))
-            throw new IllegalArgumentException("value for " + name + " has to be a number but was: " + valueObject.getClass().getSimpleName());
-        double value = ((Number) valueObject).doubleValue();
-        if (value < minValue)
-            throw new IllegalArgumentException(name + " cannot be smaller than " + minValue + ", was " + value);
-        if (value > maxValue)
-            throw new IllegalArgumentException(name + " cannot be bigger than " + maxValue + ", was " + value);
-        return value;
-    }
-
-    /**
-     * This method finds the enum in the enumClass via enum.toString
-     */
-    private static Enum getValueOf(Enum[] enumValues, String enumToString) {
-        for (Enum e : enumValues) {
-            if (e.toString().equals(enumToString)) {
-                return e;
-            }
-        }
-        throw new IllegalArgumentException("Cannot find enum " + enumToString + " in " + Arrays.toString(enumValues));
-    }
-
-    @Override
-    public String toString() {
-        return eev.getName() + ": " + Arrays.toString(values);
-    }
-}
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/ExpressionVisitor.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/ExpressionVisitor.java
new file mode 100644
index 000000000..d386d39a6
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/weighting/custom/ExpressionVisitor.java
@@ -0,0 +1,236 @@
+/*
+ *  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.weighting.custom;
+
+import com.graphhopper.json.Statement;
+import com.graphhopper.routing.ev.DefaultEncodedValueFactory;
+import com.graphhopper.routing.ev.EncodedValueLookup;
+import com.graphhopper.routing.ev.RouteNetwork;
+import com.graphhopper.routing.ev.StringEncodedValue;
+import com.graphhopper.util.Helper;
+import org.codehaus.janino.Scanner;
+import org.codehaus.janino.*;
+
+import java.io.StringReader;
+import java.util.*;
+
+import static com.graphhopper.routing.weighting.custom.CustomModelParser.IN_AREA_PREFIX;
+
+class ExpressionVisitor implements Visitor.AtomVisitor<Boolean, Exception> {
+
+    private final ParseResult result;
+    private final EncodedValueLookup lookup;
+    private final TreeMap<Integer, Replacement> replacements = new TreeMap<>();
+    private final NameValidator nameValidator;
+    private final Set<String> allowedMethods = new HashSet<>(Arrays.asList("ordinal", "getDistance", "getName",
+            "contains", "sqrt", "abs"));
+    private String invalidMessage;
+
+    public ExpressionVisitor(ParseResult result, NameValidator nameValidator, EncodedValueLookup lookup) {
+        this.result = result;
+        this.nameValidator = nameValidator;
+        this.lookup = lookup;
+    }
+
+    // allow only methods and other identifiers (constants and encoded values)
+    boolean isValidIdentifier(String identifier) {
+        if (nameValidator.isValid(identifier)) {
+            if (!Character.isUpperCase(identifier.charAt(0)))
+                result.guessedVariables.add(identifier);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public Boolean visitRvalue(Java.Rvalue rv) throws Exception {
+        if (rv instanceof Java.AmbiguousName) {
+            Java.AmbiguousName n = (Java.AmbiguousName) rv;
+            if (n.identifiers.length == 1) {
+                String arg = n.identifiers[0];
+                if (arg.startsWith(IN_AREA_PREFIX)) {
+                    int start = rv.getLocation().getColumnNumber() - 1;
+                    replacements.put(start, new Replacement(start, arg.length(),
+                            CustomWeightingHelper.class.getSimpleName() + ".in(this." + arg + ", edge)"));
+                    result.guessedVariables.add(arg);
+                    return true;
+                } else {
+                    // e.g. like road_class
+                    if (isValidIdentifier(arg)) return true;
+                    try {
+                        new DefaultEncodedValueFactory().create(arg);
+                        invalidMessage = "encoded value '" + arg + "' not available";
+                        return false;
+                    } catch (Exception ex) {
+                    }
+                }
+            }
+            invalidMessage = "identifier " + n + " invalid";
+            return false;
+        }
+        if (rv instanceof Java.Literal)
+            return true;
+        if (rv instanceof Java.MethodInvocation) {
+            Java.MethodInvocation mi = (Java.MethodInvocation) rv;
+            if (allowedMethods.contains(mi.methodName)) {
+                // skip methods like this.in() for now
+                if (mi.target != null) {
+                    // edge.getDistance, Math.sqrt => check target name (edge or Math)
+                    Java.AmbiguousName n = (Java.AmbiguousName) mi.target.toRvalue();
+                    if (n.identifiers.length == 2 && isValidIdentifier(n.identifiers[0])) return true;
+                }
+            }
+            invalidMessage = mi.methodName + " is illegal method";
+            return false;
+        }
+        if (rv instanceof Java.BinaryOperation) {
+            Java.BinaryOperation binOp = (Java.BinaryOperation) rv;
+            int startRH = binOp.rhs.getLocation().getColumnNumber() - 1;
+            if (binOp.lhs instanceof Java.AmbiguousName && ((Java.AmbiguousName) binOp.lhs).identifiers.length == 1) {
+                String lhVarAsString = ((Java.AmbiguousName) binOp.lhs).identifiers[0];
+                boolean eqOps = binOp.operator.equals("==") || binOp.operator.equals("!=");
+                if (binOp.rhs instanceof Java.StringLiteral) {
+                    // replace String with its index for faster comparison (?) and skipping the Map<String, Integer> lookup at runtime
+                    if (lookup.hasEncodedValue(lhVarAsString)) {
+                        if (!eqOps)
+                            throw new IllegalArgumentException("Operator " + binOp.operator + " not allowed for String");
+                        StringEncodedValue ev = lookup.getStringEncodedValue(lhVarAsString);
+                        String str = ((Java.StringLiteral) binOp.rhs).value;
+                        int integ = ev.indexOf(str.substring(1, str.length() - 1));
+                        if (integ == 0) integ = -1; // 0 means not found and this should always trigger inequality
+                        replacements.put(startRH, new Replacement(startRH, str.length(), "" + integ));
+                    }
+                } else if (binOp.rhs instanceof Java.AmbiguousName && ((Java.AmbiguousName) binOp.rhs).identifiers.length == 1) {
+                    // Make enum explicit as NO or OTHER can occur in other enums so convert "toll == NO" to "toll == Toll.NO"
+                    String rhValueAsString = ((Java.AmbiguousName) binOp.rhs).identifiers[0];
+                    if (nameValidator.isValid(lhVarAsString) && Helper.toUpperCase(rhValueAsString).equals(rhValueAsString)) {
+                        if (!eqOps)
+                            throw new IllegalArgumentException("Operator " + binOp.operator + " not allowed for enum");
+                        String value = toEncodedValueClassName(binOp.lhs.toString());
+                        replacements.put(startRH, new Replacement(startRH, rhValueAsString.length(), value + "." + rhValueAsString));
+                    }
+                }
+            }
+            return binOp.lhs.accept(this) && binOp.rhs.accept(this);
+        }
+        return false;
+    }
+
+    @Override
+    public Boolean visitPackage(Java.Package p) {
+        return false;
+    }
+
+    @Override
+    public Boolean visitType(Java.Type t) {
+        return false;
+    }
+
+    @Override
+    public Boolean visitConstructorInvocation(Java.ConstructorInvocation ci) {
+        return false;
+    }
+
+    static void parseExpressions(StringBuilder expressions, NameValidator nameInConditionValidator, String exceptionInfo,
+                                 Set<String> createObjects, List<Statement> list, EncodedValueLookup lookup, String lastStmt) {
+
+        for (Statement statement : list) {
+            if (statement.getKeyword() == Statement.Keyword.ELSE) {
+                if (!Helper.isEmpty(statement.getExpression()))
+                    throw new IllegalArgumentException("expression must be empty but was " + statement.getExpression());
+
+                expressions.append("else {" + statement.getOperation().build(statement.getValue()) + "; }\n");
+            } else if (statement.getKeyword() == Statement.Keyword.ELSEIF || statement.getKeyword() == Statement.Keyword.IF) {
+                ExpressionVisitor.ParseResult parseResult = parseExpression(statement.getExpression(), nameInConditionValidator, lookup);
+                if (!parseResult.ok)
+                    throw new IllegalArgumentException(exceptionInfo + " invalid expression \"" + statement.getExpression() + "\"" +
+                            (parseResult.invalidMessage == null ? "" : ": " + parseResult.invalidMessage));
+                createObjects.addAll(parseResult.guessedVariables);
+                if (statement.getKeyword() == Statement.Keyword.ELSEIF)
+                    expressions.append("else ");
+                expressions.append("if (" + parseResult.converted + ") {" + statement.getOperation().build(statement.getValue()) + "; }\n");
+            } else {
+                throw new IllegalArgumentException("The clause must be either 'if', 'else if' or 'else'");
+            }
+        }
+        expressions.append(lastStmt);
+    }
+
+    /**
+     * Enforce simple expressions of user input to increase security.
+     *
+     * @return ParseResult with ok if it is a valid and "simple" expression. It contains all guessed variables and a
+     * converted expression that includes class names for constants to avoid conflicts e.g. when doing "toll == Toll.NO"
+     * instead of "toll == NO".
+     */
+    static ParseResult parseExpression(String expression, NameValidator validator, EncodedValueLookup lookup) {
+        ParseResult result = new ParseResult();
+        try {
+            Parser parser = new Parser(new Scanner("ignore", new StringReader(expression)));
+            Java.Atom atom = parser.parseConditionalExpression();
+            // after parsing the expression the input should end (otherwise it is not "simple")
+            if (parser.peek().type == TokenType.END_OF_INPUT) {
+                result.guessedVariables = new LinkedHashSet<>();
+                ExpressionVisitor visitor = new ExpressionVisitor(result, validator, lookup);
+                result.ok = atom.accept(visitor);
+                result.invalidMessage = visitor.invalidMessage;
+                if (result.ok) {
+                    result.converted = new StringBuilder(expression.length());
+                    int start = 0;
+                    for (Replacement replace : visitor.replacements.values()) {
+                        result.converted.append(expression, start, replace.start).append(replace.newString);
+                        start = replace.start + replace.oldLength;
+                    }
+                    result.converted.append(expression.substring(start));
+                }
+            }
+        } catch (Exception ex) {
+        }
+        return result;
+    }
+
+    static String toEncodedValueClassName(String arg) {
+        if (arg.isEmpty()) throw new IllegalArgumentException("Cannot be empty");
+        if (arg.endsWith(RouteNetwork.key(""))) return RouteNetwork.class.getSimpleName();
+        String clazz = Helper.underScoreToCamelCase(arg);
+        return Character.toUpperCase(clazz.charAt(0)) + clazz.substring(1);
+    }
+
+    static class ParseResult {
+        StringBuilder converted;
+        boolean ok;
+        String invalidMessage;
+        Set<String> guessedVariables;
+    }
+
+    interface NameValidator {
+        boolean isValid(String name);
+    }
+
+    class Replacement {
+        int start;
+        int oldLength;
+        String newString;
+
+        public Replacement(int start, int oldLength, String newString) {
+            this.start = start;
+            this.oldLength = oldLength;
+            this.newString = newString;
+        }
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/GeoToValueEntry.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/GeoToValueEntry.java
deleted file mode 100644
index 59e91ab2e..000000000
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/GeoToValueEntry.java
+++ /dev/null
@@ -1,76 +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.weighting.custom;
-
-import com.graphhopper.util.JsonFeature;
-import com.graphhopper.util.CustomModel;
-import com.graphhopper.util.EdgeIteratorState;
-import com.graphhopper.util.FetchMode;
-import com.graphhopper.util.GHUtility;
-import com.graphhopper.util.shapes.BBox;
-import com.graphhopper.util.shapes.Polygon;
-import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.prep.PreparedGeometry;
-
-final class GeoToValueEntry implements EdgeToValueEntry {
-    static final String AREA_PREFIX = "area_";
-    private final Polygon ghPolygon;
-    private final double value, elseValue;
-
-    private GeoToValueEntry(PreparedGeometry geometry, double value, double elseValue) {
-        this.ghPolygon = new Polygon(geometry);
-        this.value = value;
-        this.elseValue = elseValue;
-    }
-
-    static Geometry pickGeometry(CustomModel customModel, String key) {
-        String id = key.substring(AREA_PREFIX.length());
-        JsonFeature feature = customModel.getAreas().get(id);
-        if (feature == null)
-            throw new IllegalArgumentException("Cannot find area " + id);
-        if (feature.getGeometry() == null)
-            throw new IllegalArgumentException("Cannot find coordinates of area " + id);
-        return feature.getGeometry();
-    }
-
-    public static EdgeToValueEntry create(String name, PreparedGeometry preparedGeometry, Number value, double defaultValue,
-                                          double minValue, double maxValue) {
-        double number = value.doubleValue();
-        if (number < minValue)
-            throw new IllegalArgumentException(name + " cannot be smaller than " + minValue + ", was " + number);
-        if (number > maxValue)
-            throw new IllegalArgumentException(name + " cannot be bigger than " + maxValue + ", was " + number);
-
-        return new GeoToValueEntry(preparedGeometry, number, defaultValue);
-    }
-
-    @Override
-    public double getValue(EdgeIteratorState edgeState, boolean reverse) {
-        BBox bbox = GHUtility.createBBox(edgeState);
-        if (ghPolygon.getBounds().intersects(bbox)) {
-            if (ghPolygon.intersects(edgeState.fetchWayGeometry(FetchMode.ALL).makeImmutable()))
-                return value;
-        }
-        return elseValue;
-    }
-
-    @Override
-    public String toString() {
-        return ghPolygon.toString() + ": " + value + ", " + elseValue;
-    }
-}
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/IntToValueEntry.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/IntToValueEntry.java
deleted file mode 100644
index e610007f5..000000000
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/IntToValueEntry.java
+++ /dev/null
@@ -1,81 +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.weighting.custom;
-
-import com.graphhopper.routing.ev.IntEncodedValue;
-import com.graphhopper.util.EdgeIteratorState;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import static com.graphhopper.routing.weighting.custom.CustomWeighting.CATCH_ALL;
-import static com.graphhopper.routing.weighting.custom.DecimalToValueEntry.parseRange;
-import static com.graphhopper.routing.weighting.custom.EnumToValueEntry.getReturnValue;
-
-final class IntToValueEntry implements EdgeToValueEntry {
-    private final IntEncodedValue iev;
-    private final double minExclusive, maxExclusive;
-    private final double rangeValue;
-    private final double fallback;
-
-    private IntToValueEntry(IntEncodedValue iev, DecimalToValueEntry.Range range, double fallback) {
-        this.iev = iev;
-        this.minExclusive = range.min;
-        this.maxExclusive = range.max;
-        this.rangeValue = range.value;
-        this.fallback = fallback;
-    }
-
-    @Override
-    public double getValue(EdgeIteratorState iter, boolean reverse) {
-        double edgeValue = reverse ? iter.getReverse(iev) : iter.get(iev);
-        return edgeValue < maxExclusive && edgeValue > minExclusive ? rangeValue : fallback;
-    }
-
-    static EdgeToValueEntry create(String name, IntEncodedValue dev, Map<String, Object> map,
-                                   double defaultValue, double minValue, double maxValue) {
-        if (map.isEmpty())
-            throw new IllegalArgumentException("Empty map for " + name);
-
-        Object evEntryValue = map.get(CATCH_ALL);
-        if (evEntryValue != null)
-            defaultValue = getReturnValue(name, CATCH_ALL, evEntryValue, minValue, maxValue);
-
-        List<DecimalToValueEntry.Range> ranges = new ArrayList<>();
-        for (Map.Entry<String, Object> encValEntry : map.entrySet()) {
-            if (encValEntry.getKey() == null)
-                throw new IllegalArgumentException("key for " + name + " cannot be null, value: " + encValEntry.getValue());
-            String key = encValEntry.getKey();
-            if (CATCH_ALL.equals(key))
-                continue;
-
-            double returnValue = getReturnValue(name, key, encValEntry.getValue(), minValue, maxValue);
-            DecimalToValueEntry.Range range = parseRange(name, key, returnValue);
-            ranges.add(range);
-        }
-        if (ranges.size() != 1)
-            throw new IllegalArgumentException("Currently only one range can be specified but was " + ranges.size());
-        return new IntToValueEntry(dev, ranges.get(0), defaultValue);
-    }
-
-    @Override
-    public String toString() {
-        return iev.getName() + ", range: min:" + minExclusive + ", max:" + maxExclusive + ", value:" + rangeValue;
-    }
-}
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/PriorityCalculator.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/PriorityCalculator.java
deleted file mode 100644
index 8ba1c0055..000000000
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/PriorityCalculator.java
+++ /dev/null
@@ -1,101 +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.weighting.custom;
-
-import com.graphhopper.routing.ev.*;
-import com.graphhopper.util.CustomModel;
-import com.graphhopper.util.EdgeIteratorState;
-import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
-
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-final class PriorityCalculator {
-    private final List<EdgeToValueEntry> priorityList = new ArrayList<>();
-
-    public PriorityCalculator(CustomModel customModel, EncodedValueLookup lookup) {
-        for (Map.Entry<String, Object> entry : customModel.getPriority().entrySet()) {
-            String key = entry.getKey();
-            String priorityKey = "priority." + key;
-            Object value = entry.getValue();
-            if (value == null)
-                throw new IllegalArgumentException("Missing value for " + key + " in 'priority'");
-
-            if (key.startsWith(GeoToValueEntry.AREA_PREFIX)) {
-                if (!(value instanceof Number))
-                    throw new IllegalArgumentException(priorityKey + ": area entry requires number value but was: " + value.getClass().getSimpleName());
-                Geometry geometry = GeoToValueEntry.pickGeometry(customModel, key);
-                priorityList.add(GeoToValueEntry.create(priorityKey, new PreparedGeometryFactory().create(geometry),
-                        (Number) value, 1, 0, 1));
-            } else {
-                if (!(value instanceof LinkedHashMap))
-                    throw new IllegalArgumentException(priorityKey + ": non-root entries require a sorted map (LinkedHashMap) but was: " + value.getClass().getSimpleName());
-                final double defaultPriority = 1, minPriority = 0, maxPriority = 1;
-                EncodedValue encodedValue = getEV(lookup, "priority", key);
-                if (encodedValue instanceof EnumEncodedValue) {
-                    priorityList.add(EnumToValueEntry.create(priorityKey, (EnumEncodedValue) encodedValue,
-                            (Map) value, defaultPriority, minPriority, maxPriority));
-                } else if (encodedValue instanceof DecimalEncodedValue) {
-                    priorityList.add(DecimalToValueEntry.create(priorityKey, (DecimalEncodedValue) encodedValue,
-                            (Map) value, defaultPriority, minPriority, maxPriority));
-                } else if (encodedValue instanceof BooleanEncodedValue) {
-                    priorityList.add(BooleanToValueEntry.create(priorityKey, (BooleanEncodedValue) encodedValue,
-                            (Map) value, defaultPriority, minPriority, maxPriority));
-                } else if (encodedValue instanceof IntEncodedValue) {
-                    priorityList.add(IntToValueEntry.create(priorityKey, (IntEncodedValue) encodedValue,
-                            (Map) value, defaultPriority, minPriority, maxPriority));
-                } else {
-                    throw new IllegalArgumentException("The encoded value '" + key + "' used in 'priority' is of type "
-                            + encodedValue.getClass().getSimpleName() + ", but only types enum, decimal and boolean are supported.");
-                }
-            }
-        }
-    }
-
-    static EncodedValue getEV(EncodedValueLookup lookup, String name, String key) {
-        if (!lookup.hasEncodedValue(key))
-            throw new IllegalArgumentException("Cannot find encoded value '" + key + "' specified in '" + name
-                    + "'. Available: " + names(lookup.getEncodedValues()));
-        return lookup.getEncodedValue(key, EncodedValue.class);
-    }
-
-    private static String names(List<EncodedValue> allShared) {
-        String nameStr = "";
-        for (EncodedValue ev : allShared) {
-            nameStr += ev.getName() + ",";
-        }
-        return nameStr;
-    }
-
-    /**
-     * @return weight without unit. The lower the priority is the higher the weight of the specified edge will be.
-     */
-    public double calcPriority(EdgeIteratorState edge, boolean reverse) {
-        double priority = 1;
-        for (int i = 0; i < priorityList.size(); i++) {
-            EdgeToValueEntry entry = priorityList.get(i);
-            double value = entry.getValue(edge, reverse);
-            if (value == 0) return 0;
-            priority *= value;
-        }
-        return priority;
-    }
-}
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/SpeedCalculator.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/SpeedCalculator.java
deleted file mode 100644
index 872cc85b8..000000000
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/SpeedCalculator.java
+++ /dev/null
@@ -1,160 +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.weighting.custom;
-
-import com.graphhopper.routing.ev.*;
-import com.graphhopper.util.CustomModel;
-import com.graphhopper.util.EdgeIteratorState;
-import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
-
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import static com.graphhopper.routing.weighting.custom.GeoToValueEntry.AREA_PREFIX;
-import static com.graphhopper.routing.weighting.custom.PriorityCalculator.getEV;
-
-final class SpeedCalculator {
-    private final List<EdgeToValueEntry> speedFactorList = new ArrayList<>();
-    private final List<EdgeToValueEntry> maxSpeedList = new ArrayList<>();
-    private final DecimalEncodedValue avgSpeedEnc;
-    private final double maxSpeed;
-    private final double maxSpeedFallback;
-
-    public SpeedCalculator(final double maxSpeed, CustomModel customModel, DecimalEncodedValue avgSpeedEnc,
-                           EncodedValueLookup lookup) {
-        this.maxSpeed = maxSpeed;
-        this.maxSpeedFallback = customModel.getMaxSpeedFallback() == null ? maxSpeed : customModel.getMaxSpeedFallback();
-        this.avgSpeedEnc = avgSpeedEnc;
-        if (this.maxSpeedFallback > maxSpeed)
-            throw new IllegalArgumentException("max_speed_fallback cannot be bigger than max_speed " + maxSpeed);
-
-        // use max_speed to lower speed for the specified conditions
-        for (Map.Entry<String, Object> entry : customModel.getMaxSpeed().entrySet()) {
-            String key = entry.getKey();
-            String maxSpeedKey = "max_speed." + key;
-            Object value = entry.getValue();
-            if (value == null)
-                throw new IllegalArgumentException("Missing value for " + key + " in 'priority'");
-
-            if (key.startsWith(AREA_PREFIX)) {
-                if (!(value instanceof Number))
-                    throw new IllegalArgumentException(maxSpeedKey + ": area entry requires number value but was: " + value.getClass().getSimpleName());
-                Geometry geometry = GeoToValueEntry.pickGeometry(customModel, key);
-                maxSpeedList.add(GeoToValueEntry.create(maxSpeedKey, new PreparedGeometryFactory().create(geometry),
-                        (Number) value, maxSpeed, 0, maxSpeed));
-            } else {
-                if (!(value instanceof LinkedHashMap))
-                    throw new IllegalArgumentException(maxSpeedKey + ": non-root entries require a sorted map (LinkedHashMap), but was: " + value.getClass().getSimpleName());
-                final double defaultMaxSpeed = maxSpeed, minMaxSpeed = 0, maxMaxSpeed = maxSpeed;
-                EncodedValue encodedValue = getEV(lookup, "max_speed", key);
-                if (encodedValue instanceof EnumEncodedValue) {
-                    maxSpeedList.add(EnumToValueEntry.create(maxSpeedKey, (EnumEncodedValue) encodedValue,
-                            (Map) value, defaultMaxSpeed, minMaxSpeed, maxMaxSpeed));
-                } else if (encodedValue instanceof DecimalEncodedValue) {
-                    maxSpeedList.add(DecimalToValueEntry.create(maxSpeedKey, (DecimalEncodedValue) encodedValue,
-                            (Map) value, defaultMaxSpeed, minMaxSpeed, maxMaxSpeed));
-                } else if (encodedValue instanceof BooleanEncodedValue) {
-                    maxSpeedList.add(BooleanToValueEntry.create(maxSpeedKey, (BooleanEncodedValue) encodedValue,
-                            (Map) value, defaultMaxSpeed, minMaxSpeed, maxMaxSpeed));
-                } else if (encodedValue instanceof IntEncodedValue) {
-                    maxSpeedList.add(IntToValueEntry.create(maxSpeedKey, (IntEncodedValue) encodedValue,
-                            (Map) value, defaultMaxSpeed, minMaxSpeed, maxMaxSpeed));
-                } else {
-                    throw new IllegalArgumentException("The encoded value '" + key + "' used in 'max_speed' is of type "
-                            + encodedValue.getClass().getSimpleName() + ", but only types enum, decimal and boolean are supported.");
-                }
-            }
-        }
-
-        // use speed_factor to reduce the estimated speed value under the specified conditions
-        for (Map.Entry<String, Object> entry : customModel.getSpeedFactor().entrySet()) {
-            String key = entry.getKey();
-            String speedFactorKey = "speed_factor." + key;
-            Object value = entry.getValue();
-            if (value == null)
-                throw new IllegalArgumentException("Missing value for " + key + " in 'priority'");
-
-            if (key.startsWith(AREA_PREFIX)) {
-                if (!(value instanceof Number))
-                    throw new IllegalArgumentException(speedFactorKey + ": area entry requires number value but was: " + value.getClass().getSimpleName());
-                Geometry geometry = GeoToValueEntry.pickGeometry(customModel, key);
-                speedFactorList.add(GeoToValueEntry.create(speedFactorKey, new PreparedGeometryFactory().create(geometry),
-                        (Number) value, 1, 0, 1));
-            } else {
-                if (!(value instanceof LinkedHashMap))
-                    throw new IllegalArgumentException(speedFactorKey + ": non-root entries require a sorted map (LinkedHashMap), but was: " + value.getClass().getSimpleName());
-                final double defaultSpeedFactor = 1, minSpeedFactor = 0, maxSpeedFactor = 1;
-                EncodedValue encodedValue = getEV(lookup, "speed_factor", key);
-                if (encodedValue instanceof EnumEncodedValue) {
-                    speedFactorList.add(EnumToValueEntry.create(speedFactorKey, (EnumEncodedValue) encodedValue,
-                            (Map) value, defaultSpeedFactor, minSpeedFactor, maxSpeedFactor));
-                } else if (encodedValue instanceof DecimalEncodedValue) {
-                    speedFactorList.add(DecimalToValueEntry.create(speedFactorKey, (DecimalEncodedValue) encodedValue,
-                            (Map) value, defaultSpeedFactor, minSpeedFactor, maxSpeedFactor));
-                } else if (encodedValue instanceof BooleanEncodedValue) {
-                    speedFactorList.add(BooleanToValueEntry.create(speedFactorKey, (BooleanEncodedValue) encodedValue,
-                            (Map) value, defaultSpeedFactor, minSpeedFactor, maxSpeedFactor));
-                } else if (encodedValue instanceof IntEncodedValue) {
-                    speedFactorList.add(IntToValueEntry.create(speedFactorKey, (IntEncodedValue) encodedValue,
-                            (Map) value, defaultSpeedFactor, minSpeedFactor, maxSpeedFactor));
-                } else {
-                    throw new IllegalArgumentException("The encoded value '" + key + "' used in 'speed_factor' is of type "
-                            + encodedValue.getClass().getSimpleName() + ", but only types enum, decimal and boolean are supported.");
-                }
-            }
-        }
-    }
-
-    public double getMaxSpeed() {
-        return maxSpeed;
-    }
-
-    /**
-     * @return speed in km/h
-     */
-    public double calcSpeed(EdgeIteratorState edge, boolean reverse) {
-        double speed = reverse ? edge.getReverse(avgSpeedEnc) : edge.get(avgSpeedEnc);
-        if (Double.isInfinite(speed) || Double.isNaN(speed) || speed < 0)
-            throw new IllegalStateException("Invalid estimated speed " + speed);
-
-        for (int i = 0; i < speedFactorList.size(); i++) {
-            EdgeToValueEntry entry = speedFactorList.get(i);
-            double factorValue = entry.getValue(edge, reverse);
-            speed *= factorValue;
-            if (speed == 0) break;
-        }
-
-        boolean applied = false;
-        for (int i = 0; i < maxSpeedList.size(); i++) {
-            EdgeToValueEntry entry = maxSpeedList.get(i);
-            double maxValue = entry.getValue(edge, reverse);
-            if (speed > maxValue) {
-                applied = true;
-                speed = maxValue;
-            }
-        }
-
-        if (!applied && speed > maxSpeedFallback)
-            return maxSpeedFallback;
-
-        return Math.min(speed, maxSpeed);
-    }
-}
diff --git a/docs/core/profiles.md b/docs/core/profiles.md
index c32baccdd..7998319de 100644
--- a/docs/core/profiles.md
+++ b/docs/core/profiles.md
@@ -1,14 +1,34 @@
 # Profiles
 
+GraphHopper lets you customize how different kinds of roads shall be prioritized during its route calculations. For
+example when travelling long distances with a car you typically want to use the highway to minimize your travelling
+time. However, if you are going by bike you certainly do not want to use the highway and rather take some shorter route,
+use designated bike lanes and so on. GraphHopper provides built-in vehicle types that cover some standard vehicles. They
+can be used with a few different weightings like the 'fastest' weighting that chooses the fastest route (minimum
+travelling time), or the 'shortest' weighting that chooses the shortest route (minimum travelling distance). The
+selection of a vehicle and weighting is called 'profile', and we refer to these built-in choices as 'standard profiles'
+here. For more flexibility there is a special kind of weighting, called 'custom' weighting that allows for fine-grained
+control over which roads GraphHopper will prioritize. Such custom profiles still use one of the built-in vehicles, but
+you can modify, e.g. the travelling speed for certain road types. Both types of profiles, the standard ones and custom
+profiles, are explained in the following.
+
 ## Standard Profiles
 
-When calculating routes with GraphHopper you can customize how different kinds of roads shall be prioritized. For example
-when travelling long distances with a car you typically want to use the highway to minimize your travelling time.
-However, if you are going by bike you certainly do not want to use the highway and rather take some shorter route,
-use designated bike lanes and so on.
- 
-GraphHopper includes the following pre-configured 'vehicles', that you can choose from depending on the type of vehicle
-you want to calculate routes for: 
+Standard profiles require little configuration and all you need to choose is a unique name, a 'vehicle', and a
+'weighting' type. All profiles are specified in the 'profiles' section of `config.yml` and there has to be at least one
+profile. Here is an example:
+
+```yaml
+profiles:
+  - name: car
+    vehicle: car
+    weighting: fastest
+  - name: some_other_profile
+    vehicle: bike
+    weighting: shortest
+```
+
+The vehicle field must correspond to one of GraphHopper's built-in vehicle types:
 
 - foot
 - hike
@@ -22,86 +42,58 @@ you want to calculate routes for:
 - motorcycle
 
 By choosing a vehicle GraphHopper determines the accessibility and an average travel speed for the different road types.
-If you are interested in the low-level Java API note that these pre-configured vehicles correspond to implementations of
-the `FlagEncoder` interface. 
- 
-Besides the vehicle it is also possible to use different weightings (=cost functions) for the route calculation. 
-GraphHopper includes the following weightings:
-
-- fastest
-- shortest
+If you are interested in the low-level Java API note that the vehicles correspond to implementations of
+the `FlagEncoder` interface.
+
+The weighting determines the 'cost function' for the route calculation and must match one of the following built-in
+weightings:
+
+- fastest (minimum travel time)
+- shortest (minimum travel distance)
 - short_fastest (yields a compromise between short and fast routes)
 - curvature (prefers routes with lots of curves for enjoyable motorcycle rides)
-- custom (see the next section)
+- custom (enables custom profiles, see the next section)
 
-The different travelling modes are called 'profiles'. When starting the GraphHopper server at least one such profile
-needs to be configured upfront, which is done in the 'profiles' section of `config.yml`. Here is an example:
+The profile name is used to select the profile when executing routing queries. To do this use the `profile` request
+parameter, for example `/route?point=49.5,11.1&profile=car` or `/route?point=49.5,11.1&profile=some_other_profile`.
 
-```yaml
-profiles:
-  - name: car
-    vehicle: car
-    weighting: fastest
-  - name: some_other_profile
-    vehicle: bike
-    weighting: shortest
-```
+## Custom Profiles
 
-Every profile has to include a unique name that is used to select the profile when executing routing queries. The vehicle
-and weighting fields are also required and need to match one of the values listed above. To choose one of the profiles you
-need to use the `profile` parameter for your request, like `/route?point=49.5,11.1&profile=car` or 
-`/route?point=49.5,11.1&profile=some_other_profile` in this example.
+*Disclaimer*: Custom profiles should still be considered a beta feature. Using them should be working, but details about
+the weight formula and the meaning of the different parameters is still subject to change. Also this feature will
+strongly benefit from community feedback, so do not hesitate to share your experience, your favorite custom model or
+some of the problems you ran into when you tried custom profiles.
 
-## Speed and Hybrid Mode
+You can adjust the cost function of GraphHopper's route calculations in much more detail by using so called 'custom'
+profiles. Every custom profile builds on top of a 'base' vehicle from which the profile inherits the road accessibility
+rules and default speeds for the different road types. However, you can specify a set of rules to change these default
+values. For example you can change the speed only for a certain type of road (and much more).
 
-GraphHopper can drastically improve the execution time of the route calculations by preprocessing the routing profiles
-during import. To enable this you need to list the profiles you want GraphHopper to do the preprocessing for like this:
+Custom profiles are specified like this:
 
 ```yaml
-# profiles you want to use with speed mode need to go here
-profiles_ch:
-  - profile: car
-  - profile: some_other_profile
-
-# profiles you want to use with hybrid mode need to go here
-profiles_lm:
-  - profile: car
-  - profile: some_other_profile 
-```
-
-Note that 'CH' is short for 'Contraction Hierarchies', the underlying technique used to realize speed mode and
-'LM' is short for 'Landmarks', which is the algorithm used for the hybrid mode.
+profiles:
+  - name: my_custom_profile
+    vehicle: car
+    weighting: custom
+    custom_model_file: path/to/my_custom_profile.yaml
 
-For hybrid mode there is a special feature that allows 're-using' the prepared data for different profiles. You can
-do this like this:
-```yaml
-profiles_lm:
-  - profile: car
-  - profile: some_other_profile
-    preparation_profile: car
 ```
 
-which means that for `some_other_profile` the preparation of the `car` profile will be used. However, this will only
-give correct routing results if `some_other_profile` yields larger or equal weights for all edges than the `car` profile.
-Better do not use this feature unless you know what you are doing.  
+The name and vehicle fields are the same as for standard profiles and the vehicle field is used as the 'base' vehicle
+for the custom profile. The weighting is always set to `custom` for custom profiles. The details about the custom
+profile go into a separate YAML (or JSON) file and `custom_model_file` holds the path to this file.
 
-## Custom Profiles
+Using custom profiles for your routing requests works just the same way as for standard profiles. Simply add
+`profile=my_custom_profile` as request parameter to your routing request.
 
-*Disclaimer*: Custom profiles should still be considered a beta feature. Using them should be working, but details about
-the weight formula and the meaning of the different parameters is still subject to change. Also this feature will strongly
-benefit from community feedback, so do not hesitate with sharing your experience, custom models or problems you are 
-running into!
- 
-You can take the customization of the routing profiles much further than just selecting one of the default vehicles and
-weightings, by using 'custom' profiles that let you adjust the cost function on a much more fine-grained level.
-Using a custom profile you can make adjustments to a 'base' vehicle. By choosing the base vehicle you inherit the road
-accessibilty rules and default speeds of this vehicle, but the custom weighting gives you the freedom to overwrite 
-certain parts of the cost function depending on the different road properties. 
+We will now explain how the custom weighting works and how to create the custom_model_file.
 
 ### Custom Weighting
-The weight or 'cost' of travelling along an 'edge' (a road segment of the routing network) depends on the length
-of the road segment (the distance), the travelling speed, the 'priority' and the 'distance_influence' factor (see below).
-To be more precise, the cost function has the following form:
+
+The weight or 'cost' of travelling along an 'edge' (a road segment of the routing network) depends on the length of the
+road segment (the distance), the travelling speed, the 'priority' and the 'distance_influence' factor (see below). To be
+more precise, the cost function has the following form:
 
 ```
 edge_weight = edge_distance / (speed * priority) + edge_distance * distance_influence
@@ -114,212 +106,321 @@ the `distance_influence`, which we will discuss in a moment. First we need to ha
 ### Edge properties: Encoded Values
 
 GraphHopper assigns values of different categories ('encoded values') to each road segment. For example for OSM data 
-they are derived from the OSM way tags. The available categories are specified by using the `graph.encoded_values` field
-in `config.yml` and (unless you do further customization of GraphHopper) the possible categories are defined in [`DefaultEncodedValueFactory.java`](../../core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java).
+they are derived from the OSM way tags. All possible categories are defined in
+[`DefaultEncodedValueFactory.java`](../../core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java)
+but only categories specified with `graph.encoded_values` field in the `config.yml` are available in the graph storage.
 For example there are the following categories (some of their possible values are given in brackets).
 
-- road_class: (other,motorway,trunk,primary,secondary,track,steps,cycleway,footway,...)
-- road_environment: (road,ferry,bridge,tunnel,...)
-- road_access: (destination,delivery,private,no,...)
-- surface: (paved,dirt,sand,gravel,...)
+- road_class: (OTHER, MOTORWAY, TRUNK, PRIMARY, SECONDARY, TRACK, STEPS, CYCLEWAY, FOOTWAY, ...)
+- road_environment: (ROAD, FERRY, BRIDGE, TUNNEL, ...)
+- road_access: (DESTINATION, DELIVERY, PRIVATE, NO, ...)
+- surface: (PAVED, DIRT, SAND, GRAVEL, ...)
+- toll: (NO, ALL, HGV)
 
 To find out about all the possible values of a category you can take a look at the corresponding Java files like 
 [`RoadClass.java`](../../core/src/main/java/com/graphhopper/routing/profiles/RoadClass.java), query the
 `/info` endpoint of the server or use the auto-complete feature of the text box that opens when clicking the 'flex' icon
 in the web UI.
 
-Besides these kind of categories, which can take multiple different string values, there are also some that represent a
+Besides this kind of categories, which can take multiple different string values, there are also some that represent a
 boolean value (they are either true or false for a given edge), like:
 
 - get_off_bike
 - road_class_link
 
-And there are others that take on a numeric value, like: 
+And there are others that take on a numeric value, like:
 
 - max_weight
 - max_width
 
-*Important note: Whenever you want to use any of these categories for a custom profile you need to add them to 
+*Important note: Whenever you want to use any of these categories for a custom profile you need to add them to
 `graph.encoded_values` in `config.yml`.*
 
-### Setting up a Custom Model
+### Creating the `custom_model_file`: Setting up a custom model
+
+As we saw in one of the previous sections, the custom weighting function has three parameters that you can adjust:
+speed, priority and distance_influence. You can set up rules that determine these parameters from the edge's properties.
+A set of such rules is called a 'custom model' and it is written in a dedicated YAML or JSON file,
+the `custom_model_file`. We will explain how to set up these rules. Customizing speed and priority is very similar, so
+we will explain most of the details you need to know for speed, but they can be applied very much the same way for
+priority as well.
+
+#### The custom model language by the example of customizing `speed`
+
+For every road segment a default speed is inherited from the profile's (base) vehicle. Adjusting the speed is done by a
+series of conditional operations that are written in the `speed` section of the `custom_model_file` and modify the
+default speed. Currently the custom model language only comprises two operators:
+
+- `multiply by` multiplies the speed value with a given number
+- `limit to` limits the speed value to a given number
+
+The operations are applied to the default speed from top to bottom, i.e. operations that are lower in the list are
+applied to the resulting value of previous operations. Each operation is only executed if the corresponding condition
+applies for the edge the speed is calculated for.
 
-As mentioned above, the custom weighting function has three parameters that you can adjust: speed, priority and
-distance_influence. You can set up rules that determine these parameters from the edge's properties. A set of such rules
-is called a 'custom model' and it is written in a dedicated YAML format. We will now see how the cost function parameters
-can be influenced by the different fields of such a custom model.
-  
-#### Customizing `speed`
+##### `if` statements and the `multiply by` operation
+
+Let's start with a simple example using `multiply by`:
 
-For every edge a default speed is inherited from the base vehicle, but you have multiple options to adjust it.
-The first thing you can do is rescaling the default speeds using the `speed_factor` section. For example this is how you
-can reduce the the speed of every edge that has the value 'motorway' for the category 'road_class' to fifty percent of 
-the default speed that is normally used by the base vehicle for this road class:
 ```yaml
-speed_factor:
-  road_class: {motorway: 0.5}
-```  
-Note that `road_class: {motorway: 0.5}` is an alternative YAML notation that is equivalent to:
+# this is the `custom_model_file`.
+speed:
+  - if: road_class == MOTORWAY
+    multiply by: 0.5
+```
+
+This custom model reduces the speed of every road segment that has the value `MOTORWAY` for the category 'road_class' to
+fifty percent of the default speed (the default speed is multiplied by `0.5`). Again, the default speed is the speed
+that GraphHopper would normally use for the profile's vehicle. Note the `if` clause which means that the
+operation (`multiply_by`) is only applied *if* the condition `road_class == MOTORWAY`
+is fulfilled for the edge under consideration. The `==` indicates equality, i.e. the condition reads "the road_class
+equals MOTORWAY". If you're a bit familiar with programming note that the condition (the value of the `if` key) is just
+a boolean condition in Java language (other programming languages like C or JavaScript are very similar in this regard).
+A more complex condition could look like this: `road_class == PRIMARY || road_class == TERTIARY` which uses the **or**
+(`||`) operator and literally means "road_class equals PRIMARY or road_class equals TERTIARY".
+
+As already stated there can be multiple such 'if statements' in the speed section and they are evaluated from top to
+bottom:
+
 ```yaml
-speed_factor:
-  road_class:
-    motorway: 0.5
+speed:
+  - if: road_class == MOTORWAY
+    multiply by: 0.5
+  - if: road_class == PRIMARY || road_environment == TUNNEL
+    multiply by: 0.7
 ```
 
-You can also setup speed factors for multiple road classes like this
+In this example the default speed of edges with `road_class == MOTORWAY` will be multiplied by `0.5`, the default speed
+of edges with `road_class == PRIMARY` will be multiplied by `0.7` and for edges with both `road_class == MOTORWAY` and
+`road_environment == TUNNEL` the default speed will be multiplied first by `0.5` and then by `0.7` (the default speed
+will be multiplied with `0.35` overall). For edges with `road_class == PRIMARY` and `road_environment == TUNNEL` we only
+multiply by `0.7`, even though both parts of the second condition apply. It only matters whether the edge matches the
+condition or not.
+
+`road_class` and `road_environment` are categories of 'enum' type, i.e. their value can only be one of a fixed set of
+values, like `MOTORWAY` for `road_class`. The possible values were listed in one of the previous sections.
+
+Other categories like `get_off_bike` are of `boolean` type. They can be used as conditions directly, for example:
+
 ```yaml
-speed_factor:
-  road_class: {motorway: 0.5, primary: 0.7, tertiary: 0.9}
+speed:
+  - if: get_off_bike
+    multiply by: 0.6
 ```
 
-and use multiple categories to influence the speed factor
+which means that for edges with `get_off_bike==true` the speed factor will be `0.6`.
+
+For categories/encoded values with numeric values, like `max_width` you should not use the `==` (equality) or `!=` (
+inequality)
+operators, but the numerical comparison operators "bigger" `>`, "bigger or equals" `>=`, "smaller" `<`, or "smaller or
+equals" `<=`, e.g.:
+
 ```yaml
-speed_factor:
-  road_class: {motorway: 0.5}
-  road_environment: {tunnel: 0.8}
+speed:
+  - if: max_width < 2.5
+    multiply by: 0.8
+``` 
+
+which means that for all edges with `max_width` smaller than `2.5m` the speed is multiplied by `0.8`.
+
+Categories of `string` type are used like this (note the quotes ""):
+
+```yaml
+speed:
+  - if: country == "DEU"
+    multiply by: 0
 ```
 
-If an edge matches multiple rules the speed factor values will be multiplied. For example, here the speed factor of 
-a road segment that has `road_class=motorway` will be `0.5`, the speed factor of a road segment that additionally has 
-`road_environment=tunnel` will be `0.4` and the speed factor of a road segment that has `road_class=secondary` and 
-`road_environment=tunnel` will be `0.8`.
+##### The `limit to` operation
+
+As already mentioned, besides the `multiply by` operator there is also the `limit to` operator. As the name suggests
+`limit to` limits the current value to the given value. Take this example:
 
-Instead of setting the speed factors for certain values you can instead set the speed factors for all *other* values using
-as special key (`"*"`), like this:
 ```yaml
-speed_factor: 
-  road_class: {"*": 0.5}
-  road_environment: {tunnel: 0.8, "*": 0.6}
+speed:
+  - if: road_class == MOTORWAY
+    multiply_by: 0.8
+  - if: surface == GRAVEL
+    limit to: 60
 ```
 
-So in this example we set a speed factor of `0.5` regardless of the `road_class` and all `road_environment` values yield
-a speed factor of `0.6` *except* `tunnel` which gets a speed factor of `0.8`. And as mentioned above for edges that match
-multiple of these rules the different factors get multiplied.
+This implies that on all road segments with the `GRAVEL` value for `surface` the speed will be at most `60km/h`,
+regardless of the default speed and the previous rules. So for a road segment with `road_class == MOTORWAY`,
+`surface == GRAVEL` and default speed `100` the first statement reduces the speed from `100` to `80` and the second
+statement further reduces the speed from `80` to `60`. If the `road_class` was `PRIMARY` and the default speed was `50`
+the first rule would not apply and the second rule would do nothing, because limiting `50` to `60` is still `50`.
+
+Note that all values used for `limit to` must be in the range `[0, max_vehicle_speed]` where `max_vehicle_speed` is the
+maximum speed that is set for the base vehicle and cannot be changed.
+
+A common use-case for the `limit to` operation is the following pattern:
 
-For encoded values with boolean values, like `get_off_bike` you set the speed factor like this:
 ```yaml
-speed_factor:
-  get_off_bike: {true: 0.6, false: 1.0}
+speed:
+  - if: true
+    limit to: 90
 ```
-which means that for edges with `get_off_bike=true` the speed factor will be `0.6` and otherwise it will be `1.0`.
-You can skip any of these values to retain the default.
 
-For encoded values with numeric values, like `max_width` you use the `<` and `>` operators, like this:
+which means that the speed is limited to `90km/h` for all road segments regardless of its properties. The condition
+`true` is always fulfilled.
+
+##### `else` and `else if` statements
+
+The `else` statement allows you to define that some operations should be applied if an edge does **not** match a
+condition. So this example:
+
 ```yaml
-speed_factor:
-  max_width: {"<2.5": 0.8}
-``` 
-which means that for all edges with `max_width` smaller than `2.5m` the speed factor is `0.8`.
+speed:
+  - if: road_class == MOTORWAY
+    multiply by: 0.5
+  - else:
+    limit to: 50
+```
 
-In any case values of `speed_factor` have to be in the range `[0,1]` and it is not possible to *increase* the speed for
-edges of certain types. 
+means that for all edges with `road_class == MOTORWAY` we multiply the default speed by `0.5` and for all others we
+limit the default speed to `50` (but never both). In case you want to distinguish more than two cases (edges that match
+or match not a condition) you can use `else if` statements which are only evaluated in case the previous `if`
+or `else if` statement did **not** match:
 
-Another way to change the speed is using the `max_speed` section, for example:
 ```yaml
-max_speed:
-  surface: {gravel: 60}
+speed:
+  - if: road_class == MOTORWAY
+    multiply by: 0.5
+  - else if: road_environment == TUNNEL
+    limit to: 70
+  - else:
+    multiply by: 0.9
 ```
 
-implies that on all road segments with `surface=gravel` the speed will be at most `60km/h`, regardless of the default 
-speed of this edge or the adjustments made by the `speed_factor` section. Just like with `speed_factor` you can setup
-`max_speed` values for multiple category values and different categories. If multiple rules match for a given edge the
-most restrictive rule will determine the speed (the minimum `max_speed` will be applied). 
-Values for `max_speed` must be in the range `[0,max_vehicle_speed]` where `max_vehicle_speed` is the maximum speed that
-is set for the base vehicle (which you cannot change).
+So if the first condition matches (`road_class == MOTORWAY`) the default speed is multiplied by `0.5` but the other two
+statements are ignored. Only if the first statement does not match (e.g. `road_class == PRIMARY`) the second statement
+is even considered and only if it matches (`road_environment == TUNNEL`) the default speed is limited to 70. The last
+operation (`multiply by: 0.9`) is only applied if the two previous conditions did not match.
+
+`else` and `else if` statements always require a preceding `if` or `else if` statement. However, there can be multiple
+'blocks' of subsequent `if/else if/else` statements in the list of rules for `speed`.
 
-You can also modify the speed for all edges in a certain area. To do this first add some areas to the `areas` section
-of the custom model and then use this name to set a `speed_factor` or `max_speed` for this area. In the following
-example we set the `speed_factor` of an area called `my_area` to `0.7`. For `max_speed` it works the same way. All area
-names need to be prefixed with `area_`.  
+`else if` is useful for example in case you have multiple `multiply by` operations, but you do not want that the speed
+gets reduced by all of them. For the following model
 
 ```yaml
-speed_factor:
-  area_my_area: 0.7
+speed:
+  - if: road_class == MOTORWAY
+    multiply by: 0.5
+  - else if: road_environment == TUNNEL
+    multiply by: 0.8
+```
+
+only the first factor (`0.5`) will be applied even for road segments that fulfill both conditions.
+
+##### `areas`
+
+You can not only modify the speed of road segments based on properties as we saw in the previous examples, but you can
+also modify the speed of road segments based on their location. To do this you need to first create and add some areas
+to the `areas` section of in the custom model file. You can then use the name of these areas in the condition we used in
+the `if/else/else if` statements we saw in the previous sections.
+
+In the following example we multiply the speed of all edges in an an area called `custom1` with `0.7` and also limit it
+to `50km/h`. Note that each area's name needs to be prefixed with `in_area_`:
 
-max_speed:
-  area_my_area: 50
+```yaml
+speed:
+  - if: in_area_custom1
+    multiply by: 0.7
+  - if: in_area_custom1
+    limit to: 50
 
 areas:
-  my_area:
+  custom1:
     type: "Feature"
     geometry:
       type: "Polygon"
       coordinates: [
-        [10.75, 46.65],
-        [9.54, 45.65],
-        [10.75, 44.65],
+        [ 10.75, 46.65 ],
+        [ 9.54, 45.65 ],
+        [ 10.75, 44.65 ],
         [8.75, 44.65],
         [8.75, 45.65],
         [8.75, 46.65]
       ]
 ```
 
-The areas are given in GeoJson format. Using the `areas` feature you can also block entire areas completely, but you 
-should rather use the `priority` section for this (see below).
-
-The last custom model field you can to customize the speed is the `max_speed_fallback`. By default this is set to the
-maximum speed of the base vehicle. It allows setting a global maximum for the speeds, so for example
-```yaml
-max_speed_fallback: 50
-```
-means that the speed is at most `50km/h` for any edge regardless of its properties. 
+The areas are given in GeoJson format. Note that JSON can be directly copied into YAML without further modifications.
+Using the `areas` feature you can also block entire areas i.e. by multiplying the speed with `0`, but for this you
+should rather use the `priority` section that we will explain next.
 
 #### Customizing `priority`
 
-Looking at the custom cost function formula above might make you wonder what the difference between speed and priority is,
-because it enters the formula in the same way. When calculating the edge weights (which determine the optimal route)
-changing the speed in fact has the same effect as changing the priority. But do not forget that GraphHopper not 
-only calculates the optimal route, but also the time (and distance) it takes to drive (or walk) this route. Changing the
-speeds also means changing the resulting travelling times, but `priority` allows you to alter the route calculation 
-*without* changing the travelling time of a given route.  
+Looking at the custom cost function formula above might make you wonder what the difference between `speed`
+and `priority` is, because it enters the formula in the same way. When calculating the edge weights
+(which determine the optimal route) changing the speed in fact has the same effect as changing the priority. However, do
+not forget that GraphHopper not only calculates the optimal route, but also the time (and distance) it takes to drive (
+or walk) this route. Changing the speeds also means changing the resulting travel times, but `priority`
+allows you to alter the route calculation *without* changing the travel time of a given route.
+
+By default, the priority is `1` for every edge, so without doing anything it does not affect the weight. However,
+changing the priority of a road can yield a relative weight difference in comparison to other roads.
 
-By default the priority is `1` for every edge,
-so without doing anything it does not affect the weight. However, changing the priority for certain kinds of roads yields
-a relative weight difference depending on the edges' properties.
+Customizing the `priority` works very much like changing the `speed`, so in case you did not read the section about
+`speed` now should be the time to do this. The only real difference is that there is no `limit to` operator for
+`priority`. As a quick reminder here is an example for priority:
 
-You change the `priority` very much like you change the `speed_factor`, so
 ```yaml
 priority:
-  road_class: {motorway: 0.5, secondary: 0.9}
-  road_environment: {tunnel: 0.1}
+  - if: road_class == MOTORWAY
+    multiply by: 0.5
+  - else if: road_class == SECONDARY
+    multiply by: 0.9
+  - if: road_environment == TUNNEL
+    multiply by: 0.1
 ```
 
-means that road segments with `road_class=motorway` and `road_environment=tunnel` get priority `0.5*0.1=0.05` and those 
-with `road_class=secondary` get priority `0.9` and so on.
+means that road segments with `road_class==MOTORWAY` and `road_environment==TUNNEL` get priority `0.5*0.1=0.05` and
+those with `road_class==SECONDARY` and no TUNNEL, get priority `0.9` and so on.
+
+Edges with lower priority values will be less likely part of the optimal route calculated by GraphHopper, higher values
+mean that these road segments shall be preferred. If you do not want to state which road segments shall be avoided, but
+rather which ones shall be preferred, you need to **decrease** the priority of others:
 
-Edges with lower priority values will be less likely part of the optimal route calculated by GraphHopper, higher values 
-mean that these kind of road segments shall be preferred. To prefer certain roads you need to `decrease` the priority of
-others, which you can do using the special key `"*"`, i.e. 
 ```yaml
-priority: 
-  road_class: {cycleway: 1.0, "*": 0.8}
+priority:
+  - if: road_class != CYCLEWAY
+    multiply by: 0.8
 ```
-means decreasing the priority for all road_classes *except* cycleways. `priority` values need to be in the range `[0, 1]`.
-Just like we saw for `speed_factor` and `max_speed` you can also adjust the priority for all edges in a certain area.
-It works the same way:
+
+means decreasing the priority for all road_classes *except* cycleways.
+
+Just like we saw for `speed` you can also adjust the priority for road segments in a certain area. It works exactly the
+same way:
 
 ```yaml
 priority:
-  area_my_area: 0.7 
+  - if: in_area_custom1
+    multiply by: 0.7
 ```
 
-To block an entire area completely set the priority value to `0`. Some other useful encoded values to restrict access
-to certain roads depending on your vehicle dimensions are the following:
+To block an entire area completely set the priority value to `0`. Some other useful encoded values to restrict access to
+certain roads depending on your vehicle dimensions are the following:
+
 ```yaml
-priority: 
-  max_width: {"<2.5": 0}
-  max_length: {"<10.0": 0}
-  max_weight: {"<3.5": 0}
+priority:
+- if: max_width < 2.5
+  multiply by: 0
+- if: max_length < 10
+  multiply by: 0
+- if: max_weight < 3.5
+  multiply by: 0
 ```
-which means that the priority for all edges that allow a maximum vehicle width of `2.5m`, a maximum vehicle length of 
-`10m` or a maximum vehicle weight of `3.5tons` is zero (these edges are blocked).
+which means that the priority for all road segments that allow a maximum vehicle width of `2.5m`, a maximum vehicle
+length of `10m` or a maximum vehicle weight of `3.5tons`, or less, is zero, i.e. these "tight" road segments are
+blocked.
 
 #### Customizing `distance_influence`
 
 `distance_influence` allows you to control the trade-off between a fast route (minimum time) and a short route
-(minimum distance). Setting it to `0` means that GraphHopper will return the fastest possible route, *unless* you set 
-the priority of any edges to something different than `1.0`. Using the priority factors will always to some part favor
-some routes because they are shorter as well. Higher values of `distance_influence` will prioritize routes that are fast
+(minimum distance). Setting it to `0` means that GraphHopper will return the fastest possible route, *unless* you set
+the priority of any edges to something other than `1.0`. Using the priority factors will always to some part favor some
+routes because they are shorter as well. Higher values of `distance_influence` will prioritize routes that are fast
 (but maybe not fastest), but at the same time are short in distance.
 
 You use it like this:
@@ -328,60 +429,205 @@ distance_influence: 100
 ``` 
 
 The default is `70`. More precisely, by specifying the `distance_influence` you tell the routing engine how much time
-you need to save on a detour (a longer distance route) such that you prefer taking the detour compared to a shorter 
-distance route. Assuming that all priorities are `1` a value of zero means that no matter how little time you can save 
+you need to save on a detour (a longer distance route) such that you prefer taking the detour compared to a shorter
+distance route. Assuming that all priorities are `1` a value of zero means that no matter how little time you can save
 when doing a detour you will take it, i.e. you always prefer the fastest route. A value of `30` means that one extra
-kilometer must save you `30s` of travelling time. Or to put it another way if a reference route takes `600s` and is 
+kilometer must save you `30s` of travelling time. Or to put it another way if a reference route takes `600s` and is
 `10km` long, `distance_influence=30` means that you are willing to take an alternative route that is `11km` long only if
-it takes no longer than `570s` (saves `30s`).      
+it takes no longer than `570s` (saves `30s`).
 
-## Setting up a Custom Profile
+#### Using JSON instead of YAML notation
 
-Custom profiles work very much the same way as the 'standard' profiles described at the beginning of this document. The
-custom model needs to be written in a separate YAML (or JSON) file and the `weighting` has to be set to `custom`. Use the 
-`custom_model_file` field to point to the custom model file like this:
+You can use a slightly shorter syntax than the one we used in the previous examples, e.g.:
 
 ```yaml
-profiles:
-  - name: my_custom_profile
-    vehicle: car
-    weighting: custom
-    custom_model_file: path/to/my_custom_profile.yaml
-  
+speed:
+  - if: "road_class == MOTORWAY",     multiply by: 0.5
+  - if: "road_environment == TUNNEL", multiply by: 0.8
 ```
 
-Selecting a custom profile also works like selecting a standard profile. In our example just add `profile=my_custom_profile`
-to your routing request. To set up hybrid- or speed-mode, simply use the `profiles_ch/lm` sections and add the name of
-your custom profile (just like you do for standard profiles).
+You can even use JSON language like this:
+
+```json
+{
+  "speed": [
+    {
+      "if": "road_class == MOTORWAY",
+      "multiply by": 0.5
+    },
+    {
+      "if": "road_environment == TUNNEL",
+      "multiply by": 0.8
+    }
+  ]
+}
+```
 
-## Changing the Custom Profile for a single routing request
+To use the `else` statement in JSON you need to use the `null` value where in YAML you could leave the value blank:
 
-With flex- and hybrid mode its even possible to define the custom model on a per-request basis and doing this you can 
-even adjust a custom model configured on the server side for a single request. To do this you first set up a 'base' 
-custom profile in your server configuration, like:
+```yaml
+speed:
+  - if: road_class == MOTORWAY
+    multiply by: 0.6
+  - else:
+    multiply by: 0.8
+```
+
+becomes
+
+```json
+{
+  "speed": [
+    {
+      "if": "road_class == MOTORWAY",
+      "multiply by": 0.6
+    },
+    {
+      "else": null,
+      "multiply by": 0.8
+    }
+  ]
+}
+```
+
+## Speed and Hybrid Mode
+
+GraphHopper can drastically improve the execution time of the route calculations by preprocessing the routing profiles
+during import. To enable this you need to list the profiles you want GraphHopper to do the preprocessing for like this:
+
+```yaml
+# profiles you want to use with speed mode need to go here
+profiles_ch:
+  - profile: car
+  - profile: some_other_profile
+
+# profiles you want to use with hybrid mode need to go here
+profiles_lm:
+  - profile: car
+  - profile: some_other_profile 
+```
+
+The values given under `profile` are just the profile names you specified in the profile definitions. Note that 'CH' is
+short for 'Contraction Hierarchies', the underlying technique used to realize speed mode and
+'LM' is short for 'Landmarks', which is the algorithm used for the hybrid mode.
+
+For hybrid mode there is a special feature that allows 're-using' the prepared data for different profiles. You can do
+this like this:
+
+```yaml
+profiles_lm:
+  - profile: car
+  - profile: some_other_profile
+    preparation_profile: car
+```
+
+which means that for `some_other_profile` the preparation of the `car` profile will be used. However, this will only
+give correct routing results if `some_other_profile` yields larger or equal weights for all edges than the `car`profile.
+Better do not use this feature unless you know what you are doing.
+
+## Using different custom models on a per-request basis
+
+So far we talked only about standard and custom profiles that are configured on the server side in `config.yml`.
+However, with flex- and hybrid mode it is even possible to define the custom model on a per-request basis. This enables
+you to perform route calculations using custom models that you did not anticipate when setting up the server.
+
+To use this feature you need to query the `/route-custom` (*not* `/route`) endpoint and send the custom model along with
+your request in JSON format. The syntax for the custom model is the same as for server-side custom models (but using
+JSON not YAML notation, see above). The routing request has the same format as for `/route`, but with an
+additional `model` field that contains the custom model. You still need to set the `profile` parameter for your request
+and the given profile must be a custom profile.
+
+Now you might be wondering which custom model is used, because there is one set for the route request, but there is also
+the one given for the profile that we specify via the `profile` parameter. The answer is "both" as the two custom models
+are merged into one. The two custom models are merged according to the following rules:
+
+* all expressions in the custom model of the query are appended to the existing custom model.
+* for the custom model of the query all values of `multiply by` need to be within the range of `[0, 1]` otherwise an
+  error will be thrown
+* the `distance_influence` of the query custom model overwrites the one from the server-side custom model *unless*
+  it is not specified. However, the given value must not be smaller than the existing one.
+
+If you're curious about the second rule note that for the Hybrid mode (using Landmarks not just Dijkstra or A*)
+the merge process has to ensure that all weights resulting from the merged custom model are equal or larger than those
+of the base profile that was used during the preparation process. This is necessary to maintain the optimality of the
+underlying routing algorithm.
+
+So say your routing request (POST /route-custom) looks like this:
+
+```json
+{
+  "points": [
+    [
+      11.58199,
+      50.0141
+    ],
+    [
+      11.5865,
+      50.0095
+    ]
+  ],
+  "profile": "my_custom_car",
+  "model": {
+    "speed": [
+      {
+        "if": "road_class == MOTORWAY",
+        "multiply by": 0.8
+      },
+      {
+        "else": null,
+        "multiply by": 0.9
+      }
+    ],
+    "priority": [
+      {
+        "if": "road_environment == TUNNEL",
+        "multiply by": 0.95
+      }
+    ],
+    "distance_influence": 0.7
+  }
+}
+```
+
+where in `config.yml` we have:
 
 ```yaml
 profiles:
-  - name: my_flexible_car_profile
+  - name: my_custom_car
     vehicle: car
     weighting: custom
-    custom_model_file: my_custom_model.yml
-``` 
+    custom_model_file: path/to/my_custom_car.yml
+```
+
+and `my_custom_car.yml` looks like this:
+
+```yaml
+speed:
+  - if: surface == GRAVEL
+    limit to: 100
+```
+
+then the resulting custom model used for your request will look like this:
+
+```yaml
+speed:
+  - if: surface == GRAVEL
+    limit to: 100
+  # this was appended due to the custom model given by the request
+  - if: road_class == MOTORWAY
+    multiply by: 0.8
+  - else:
+    multiply by: 0.9
+
+priority:
+  - if: road_environment == TUNNEL
+    multiply by: 0.95
+
+distance_influence: 0.7
+```
+
+You do not necessarily need to define a proper custom model on the server side, but you can also use the special
+value `custom_model_file: empty` which means an empty custom model containing no statements will be used on the
+server-side. This way you can leave it entirely up to the user/client how the custom model shall look like.
+
 
-You do not necessarily need to define a proper custom model here and instead you can also set `custom_model_file: empty`
-(which means an empty custom model containing no rules will be used on the server-side). You then use the `/route-custom`
-(*not* `/route`) endpoint and send your custom model (using the format explained above, but as JSON) with the request 
-body. The model you send will be merged with the profile you select using the `profile` parameter (which has to be 
-a custom profile). For the Hybrid mode (not only "pure" Dijkstra or A*) the merge process has to ensure that all weights
-resulting from the merged custom model are equal or larger than those of the base profile that was used during the 
-preparation process. This is necessary to maintain the optimality of the underlying routing algorithm. 
-Therefore we use the following rules to merge the two models:
-
- * for priority: an existing factor is multiplied with the factor specified in the request
- * for speed_factor: an existing factor is multiplied with the factor specified in the request
- * for max_speed: an existing speed value is replaced if it is higher than the speed value specified in the request. 
-   If it is smaller an exception is thrown.
- * Comparisons like `max_weight: { "<3.5": 0 }` are special cases. The merge will be only accepted if the factor in the
-   query is 0. And the range can be only "expanded", which means:
-    * The 'smaller than' comparison key (like in this example the `<3.5`) can only be replaced by bigger comparison keys like `<4.5`.
-    * The 'greater than' comparison key (e.g. `>2`) can only be replaced by smaller comparison keys like `>1.9`.
diff --git a/example/src/main/java/com/graphhopper/example/RoutingExample.java b/example/src/main/java/com/graphhopper/example/RoutingExample.java
index 6c4a0ce84..f07b873ba 100644
--- a/example/src/main/java/com/graphhopper/example/RoutingExample.java
+++ b/example/src/main/java/com/graphhopper/example/RoutingExample.java
@@ -8,14 +8,17 @@ import com.graphhopper.config.CHProfile;
 import com.graphhopper.config.LMProfile;
 import com.graphhopper.config.Profile;
 import com.graphhopper.reader.osm.GraphHopperOSM;
-import com.graphhopper.routing.ev.RoadClass;
-import com.graphhopper.util.CustomModel;
 import com.graphhopper.routing.util.EncodingManager;
 import com.graphhopper.routing.weighting.custom.CustomProfile;
 import com.graphhopper.util.*;
 import com.graphhopper.util.shapes.GHPoint;
 
-import java.util.*;
+import java.util.Arrays;
+import java.util.Locale;
+
+import static com.graphhopper.json.Statement.If;
+import static com.graphhopper.json.Statement.Op.LIMIT;
+import static com.graphhopper.json.Statement.Op.MULTIPLY;
 
 public class RoutingExample {
     public static void main(String[] args) {
@@ -25,6 +28,9 @@ public class RoutingExample {
         speedModeVersusFlexibleMode(hopper);
         headingAndAlternativeRoute(hopper);
         customizableRouting(relDir + "core/files/andorra.osm.pbf");
+
+        // release resources to properly shutdown or start a new instance
+        hopper.close();
     }
 
     static GraphHopper createGraphHopperInstance(String ghLoc) {
@@ -91,7 +97,7 @@ public class RoutingExample {
                 addPoint(new GHPoint(42.508774, 1.535414)).addPoint(new GHPoint(42.506595, 1.528795)).
                 setHeadings(Arrays.asList(180d, 90d)).
                 // use flexible mode (i.e. disable contraction hierarchies) to make heading and pass_through working
-                putHint(Parameters.CH.DISABLE, true);
+                        putHint(Parameters.CH.DISABLE, true);
         // if you have via points you can avoid U-turns there with
         // req.getHints().putObject(Parameters.Routing.PASS_THROUGH, true);
         GHResponse res = hopper.route(req);
@@ -139,10 +145,10 @@ public class RoutingExample {
         // and also the blog posts https://www.graphhopper.com/?s=customizable+routing
         CustomModel model = new CustomModel();
         req.putHint(CustomModel.KEY, model);
-        Map<String, Double> map = new LinkedHashMap<>();
-        model.setMaxSpeedFallback(100d);
-        model.getPriority().put(RoadClass.KEY, map);
-        map.put(RoadClass.PRIMARY.toString(), 0.5);
+        model.addToPriority(If("road_class == PRIMARY", MULTIPLY, 0.5));
+
+        // unconditional limit to 100km/h
+        model.addToPriority(If("true", LIMIT, 100));
 
         res = hopper.route(req);
         if (res.hasErrors())
diff --git a/web-api/src/main/java/com/graphhopper/jackson/GraphHopperModule.java b/web-api/src/main/java/com/graphhopper/jackson/GraphHopperModule.java
index 37391f25e..f7b08519a 100644
--- a/web-api/src/main/java/com/graphhopper/jackson/GraphHopperModule.java
+++ b/web-api/src/main/java/com/graphhopper/jackson/GraphHopperModule.java
@@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
 import com.graphhopper.GHRequest;
 import com.graphhopper.GHResponse;
 import com.graphhopper.ResponsePath;
+import com.graphhopper.json.Statement;
 import com.graphhopper.util.InstructionList;
 import com.graphhopper.util.details.PathDetail;
 import com.graphhopper.util.shapes.GHPoint;
@@ -30,6 +31,7 @@ public class GraphHopperModule extends SimpleModule {
 
     public GraphHopperModule() {
         setMixInAnnotation(GHRequest.class, GHRequestMixIn.class);
+        addDeserializer(Statement.class, new StatementDeserializer());
         addDeserializer(GHResponse.class, new GHResponseDeserializer());
         addDeserializer(ResponsePath.class, new ResponsePathDeserializer());
         addDeserializer(Envelope.class, new JtsEnvelopeDeserializer());
diff --git a/web-api/src/main/java/com/graphhopper/jackson/StatementDeserializer.java b/web-api/src/main/java/com/graphhopper/jackson/StatementDeserializer.java
new file mode 100644
index 000000000..481746f3c
--- /dev/null
+++ b/web-api/src/main/java/com/graphhopper/jackson/StatementDeserializer.java
@@ -0,0 +1,42 @@
+package com.graphhopper.jackson;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.graphhopper.json.Statement;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+public class StatementDeserializer extends JsonDeserializer<Statement> {
+    @Override
+    public Statement deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+        JsonNode treeNode = p.readValueAsTree();
+        Statement.Op jsonOp = null;
+        double value = Double.NaN;
+        for (Statement.Op op : Statement.Op.values()) {
+            if (treeNode.has(op.getName()) && treeNode.get(op.getName()).isNumber()) {
+                jsonOp = op;
+                value = treeNode.get(op.getName()).asDouble();
+                break;
+            }
+        }
+        if (jsonOp == null)
+            throw new IllegalArgumentException("Cannot find operation. Must be one of " + Arrays.toString(Statement.Op.values()));
+        if (Double.isNaN(value))
+            throw new IllegalArgumentException("Value of operation " + jsonOp.getName() + " is not a number");
+
+        if (treeNode.has("if"))
+            return Statement.If(treeNode.get("if").asText(), jsonOp, value);
+        else if (treeNode.has("else if"))
+            return Statement.ElseIf(treeNode.get("else if").asText(), jsonOp, value);
+        else if (treeNode.has("else")) {
+            if (!treeNode.get("else").isNull())
+                throw new IllegalArgumentException("else cannot have expression but was " + treeNode.get("else"));
+            return Statement.Else(jsonOp, value);
+        }
+
+        throw new IllegalArgumentException("Cannot find if, else if or else for " + treeNode.toString());
+    }
+}
diff --git a/web-bundle/src/main/java/com/graphhopper/resources/InfoResource.java b/web-bundle/src/main/java/com/graphhopper/resources/InfoResource.java
index c6462918d..4b85f59b1 100644
--- a/web-bundle/src/main/java/com/graphhopper/resources/InfoResource.java
+++ b/web-bundle/src/main/java/com/graphhopper/resources/InfoResource.java
@@ -21,7 +21,6 @@ import com.graphhopper.GraphHopper;
 import com.graphhopper.GraphHopperConfig;
 import com.graphhopper.config.Profile;
 import com.graphhopper.routing.ev.*;
-import com.graphhopper.routing.util.EncodingManager;
 import com.graphhopper.storage.GraphHopperStorage;
 import com.graphhopper.util.Constants;
 import org.locationtech.jts.geom.Envelope;
@@ -96,16 +95,15 @@ public class InfoResource {
         info.import_date = storage.getProperties().get("datareader.import.date");
         info.data_date = storage.getProperties().get("datareader.data.date");
 
-        // do not list all supported encoded values like the none-shared ones or *.turn_costs
         List<EncodedValue> evList = storage.getEncodingManager().getEncodedValues();
         info.encoded_values = new LinkedHashMap<>();
         for (EncodedValue encodedValue : evList) {
             List<Object> possibleValueList = new ArrayList<>();
-            if (!EncodingManager.isSharedEncodedValues(encodedValue)) {
+            if (encodedValue.getName().contains("turn_costs")) {
                 // skip
             } else if (encodedValue instanceof EnumEncodedValue) {
-                for (Object o : ((EnumEncodedValue) encodedValue).getValues()) {
-                    possibleValueList.add(o.toString());
+                for (Enum o : ((EnumEncodedValue) encodedValue).getValues()) {
+                    possibleValueList.add(o.name());
                 }
             } else if (encodedValue instanceof BooleanEncodedValue) {
                 possibleValueList.add("true");
diff --git a/web/src/main/resources/assets/js/main-template.js b/web/src/main/resources/assets/js/main-template.js
index 09d0f0acb..57687b2e7 100644
--- a/web/src/main/resources/assets/js/main-template.js
+++ b/web/src/main/resources/assets/js/main-template.js
@@ -81,7 +81,8 @@ $(document).ready(function (e) {
         mapLayer.adjustMapSize();
     });
     $("#flex-example").click(function() {
-         $("#flex-input-text").val("speed_factor:\n  road_class:\n    motorway: 0.8\npriority:\n  road_environment:\n    tunnel: 0.0\n  road_class:\n    residential: 0.7\n  max_weight:\n    \">3\": 0.0");
+         $("#flex-input-text").val("speed:\n- if: road_class == MOTORWAY\n  multiply by: 0.8\n"
+          + "priority:\n- if: road_environment == TUNNEL\n  multiply by: 0.0\n- if: road_class == RESIDENTIAL\n  multiply by: 0.7\n- if: max_weight < 3\n  multiply by: 0.0");
          return false;
     });
 
@@ -276,8 +277,8 @@ $(document).ready(function (e) {
                         endIndex = endIndex < 0 ? cleanedText.length : endIndex;
                         var wordUnderCursor = cleanedText.substring(startIndex, endIndex);
                         if(this.selectionStart == 0 || this.value.substr(this.selectionStart - 1, 1) === "\n") {
-                           document.getElementById("ev_value").innerHTML = "<b>root:</b> speed_factor, priority, max_speed, max_speed_fallback, distance_influence, areas";
-                        } else if(wordUnderCursor === "priority" || wordUnderCursor === "speed_factor" || wordUnderCursor === "max_speed") {
+                           document.getElementById("ev_value").innerHTML = "<b>root:</b> priority, speed, distance_influence, areas";
+                        } else if(wordUnderCursor === "priority" || wordUnderCursor === "speed") {
                            document.getElementById("ev_value").innerHTML = "<b>" + wordUnderCursor + ":</b> " + Object.keys(json.encoded_values).join(", ");
                         } else if(json.encoded_values[wordUnderCursor]) {
                            document.getElementById("ev_value").innerHTML = "<b>" + wordUnderCursor + ":</b> " + json.encoded_values[wordUnderCursor].join(", ");


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/util/CustomModelTest.java b/core/src/test/java/com/graphhopper/routing/util/CustomModelTest.java
deleted file mode 100644
index b5bffb40a..000000000
--- a/core/src/test/java/com/graphhopper/routing/util/CustomModelTest.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- *  Licensed to GraphHopper GmbH under one or more contributor
- *  license agreements. See the NOTICE file distributed with this work for
- *  additional information regarding copyright ownership.
- *
- *  GraphHopper GmbH licenses this file to you under the Apache License,
- *  Version 2.0 (the "License"); you may not use this file except in
- *  compliance with the License. You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.graphhopper.routing.util;
-
-import com.graphhopper.util.CustomModel;
-import org.junit.jupiter.api.Test;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-public class CustomModelTest {
-
-    static CustomModel setValue(CustomModel model, String op, String encodedValue, double value) {
-        Map<String, Object> map = new HashMap<>();
-        map.put(op + value, 0);
-        model.getPriority().put(encodedValue, map);
-        return model;
-    }
-
-    Object getValue(CustomModel model, String encodedValue) {
-        Map map = (Map) model.getPriority().get(encodedValue);
-        if (map == null || map.isEmpty())
-            return null;
-        return map.keySet().iterator().next();
-    }
-
-    @Test
-    public void testMergeComparisonKeys() {
-        CustomModel truck = setValue(new CustomModel(), "<", "max_width", 3);
-        CustomModel car = setValue(new CustomModel(), "<", "max_width", 2);
-        CustomModel bike = setValue(new CustomModel(), "<", "max_weight", 0.02);
-
-        assertEquals("<2.0", getValue(CustomModel.merge(bike, car), "max_width"));
-        assertNull(getValue(bike, "max_width"));
-        assertNull(getValue(car, "max_weight"));
-
-        assertEquals("<3.0", getValue(CustomModel.merge(car, truck), "max_width"));
-        Exception ex = assertThrows(IllegalArgumentException.class, () -> CustomModel.merge(truck, car));
-        assertTrue(ex.getMessage().contains("max_width: only use a comparison key with a bigger value than 3.0 but was 2.0"), ex.getMessage());
-
-        CustomModel car2 = setValue(new CustomModel(), ">", "max_width", 2);
-        ex = assertThrows(IllegalArgumentException.class, () -> CustomModel.merge(truck, car2));
-        assertTrue(ex.getMessage().contains("max_width: comparison keys must match but did not: "), ex.getMessage());
-
-        Map<String, Object> map = new HashMap<>();
-        map.put("<2.0", 0.5);
-        CustomModel customModel = new CustomModel();
-        customModel.getPriority().put("max_width", map);
-        ex = assertThrows(IllegalArgumentException.class, () -> CustomModel.merge(customModel, customModel));
-        assertTrue(ex.getMessage().contains("only blocking comparisons are allowed, but query was 0.5 and server side: 0.5"), ex.getMessage());
-    }
-
-    @Test
-    public void testMergeEmptyModel() {
-        CustomModel emptyCar = new CustomModel();
-        CustomModel car = new CustomModel();
-        car.getPriority().put("road_class", createMap("primary", 2.0, "tertiary", 1.2));
-        // empty entries means "extend base" profile, i.e. primary=1
-        Map map = (Map) CustomModel.merge(emptyCar, car).getPriority().get("road_class");
-        assertEquals(2.0, map.get("primary"));
-        assertEquals(1.2, map.get("tertiary"));
-
-        map = (Map) CustomModel.merge(car, emptyCar).getPriority().get("road_class");
-        assertEquals(2.0, map.get("primary"));
-        assertEquals(1.2, map.get("tertiary"));
-    }
-
-    @Test
-    public void testMergeMap() {
-        CustomModel truck = new CustomModel();
-        truck.getPriority().put("road_class", createMap("primary", 1.5, "secondary", 2.0));
-        CustomModel car = new CustomModel();
-        car.getPriority().put("road_class", createMap("primary", 2.0, "tertiary", 1.2));
-
-        Map map = (Map) CustomModel.merge(truck, car).getPriority().get("road_class");
-        assertEquals(1.5 * 2.0, map.get("primary"));
-        assertEquals(2.0, map.get("secondary"));
-        assertEquals(1.2, map.get("tertiary"));
-
-        // do not change argument of merge method
-        assertEquals(1.5, (Double) ((Map) truck.getPriority().get("road_class")).get("primary"), .1);
-
-        truck.getPriority().put("road_class", createMap("primary", "incompatible"));
-        assertThrows(IllegalArgumentException.class, () -> CustomModel.merge(car, truck));
-    }
-
-    Map createMap(Object... objects) {
-        Map map = new HashMap();
-        for (int i = 0; i < objects.length; i += 2) {
-            map.put(objects[i], objects[i + 1]);
-        }
-        return map;
-    }
-}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/weighting/CustomWeightingTest.java b/core/src/test/java/com/graphhopper/routing/weighting/CustomWeightingTest.java
deleted file mode 100644
index e8c2876fb..000000000
--- a/core/src/test/java/com/graphhopper/routing/weighting/CustomWeightingTest.java
+++ /dev/null
@@ -1,315 +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.weighting;
-
-import com.graphhopper.util.JsonFeature;
-import com.graphhopper.routing.ev.*;
-import com.graphhopper.routing.util.CarFlagEncoder;
-import com.graphhopper.util.CustomModel;
-import com.graphhopper.routing.util.EncodingManager;
-import com.graphhopper.routing.util.FlagEncoder;
-import com.graphhopper.routing.weighting.custom.CustomWeighting;
-import com.graphhopper.storage.GraphBuilder;
-import com.graphhopper.storage.GraphHopperStorage;
-import com.graphhopper.util.EdgeIteratorState;
-import com.graphhopper.util.FetchMode;
-import com.graphhopper.util.GHUtility;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.locationtech.jts.geom.Coordinate;
-import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.GeometryFactory;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-import static com.graphhopper.routing.ev.RoadClass.*;
-import static com.graphhopper.routing.weighting.TurnCostProvider.NO_TURN_COST_PROVIDER;
-import static org.junit.jupiter.api.Assertions.*;
-
-public class CustomWeightingTest {
-
-    GraphHopperStorage graph;
-    DecimalEncodedValue avSpeedEnc;
-    DecimalEncodedValue maxSpeedEnc;
-    IntEncodedValue laneEnc;
-    EnumEncodedValue<RoadClass> roadClassEnc;
-    EncodingManager encodingManager;
-    FlagEncoder carFE;
-
-    @BeforeEach
-    public void setUp() {
-        carFE = new CarFlagEncoder().setSpeedTwoDirections(true);
-        laneEnc = new UnsignedIntEncodedValue("lanes", 2, true);
-        encodingManager = new EncodingManager.Builder().add(carFE).add(laneEnc).build();
-        avSpeedEnc = carFE.getAverageSpeedEnc();
-        maxSpeedEnc = encodingManager.getDecimalEncodedValue(MaxSpeed.KEY);
-        roadClassEnc = encodingManager.getEnumEncodedValue(KEY, RoadClass.class);
-        graph = new GraphBuilder(encodingManager).create();
-    }
-
-    @Test
-    public void speedOnly() {
-        // 50km/h -> 72s per km, 100km/h -> 36s per km
-        EdgeIteratorState edge;
-        GHUtility.setSpeed(50, 100, carFE, edge = graph.edge(0, 1).setDistance(1000));
-        assertEquals(72, createWeighting(new CustomModel().setDistanceInfluence(0)).calcEdgeWeight(edge, false), 1.e-6);
-        assertEquals(36, createWeighting(new CustomModel().setDistanceInfluence(0)).calcEdgeWeight(edge, true), 1.e-6);
-    }
-
-    @Test
-    public void withPriority() {
-        // 25km/h -> 144s per km, 50km/h -> 72s per km, 100km/h -> 36s per km
-        EdgeIteratorState slow = GHUtility.setSpeed(60, true, true, carFE, graph.edge(0, 1).setDistance(1000)).set(avSpeedEnc, 25).set(roadClassEnc, SECONDARY);
-        EdgeIteratorState medium = GHUtility.setSpeed(60, true, true, carFE, graph.edge(0, 1).setDistance(1000)).set(avSpeedEnc, 50).set(roadClassEnc, SECONDARY);
-        EdgeIteratorState fast = GHUtility.setSpeed(60, true, true, carFE, graph.edge(0, 1).setDistance(1000)).set(avSpeedEnc, 100).set(roadClassEnc, SECONDARY);
-
-        // without priority costs fastest weighting is the same as custom weighting
-        assertEquals(144, new FastestWeighting(carFE, NO_TURN_COST_PROVIDER).calcEdgeWeight(slow, false), .1);
-        assertEquals(72, new FastestWeighting(carFE, NO_TURN_COST_PROVIDER).calcEdgeWeight(medium, false), .1);
-        assertEquals(36, new FastestWeighting(carFE, NO_TURN_COST_PROVIDER).calcEdgeWeight(fast, false), .1);
-
-        Map<String, Object> map = new LinkedHashMap<>();
-        CustomModel model = new CustomModel().setDistanceInfluence(0);
-        assertEquals(144, createWeighting(model).calcEdgeWeight(slow, false), .1);
-        assertEquals(72, createWeighting(model).calcEdgeWeight(medium, false), .1);
-        assertEquals(36, createWeighting(model).calcEdgeWeight(fast, false), .1);
-
-        // if we reduce the priority we get higher edge weights
-        map.put(SECONDARY.toString(), 0.5);
-        model.getPriority().put(KEY, map);
-        // the absolute priority costs depend on the speed, so setting priority=0.5 means a lower absolute weight
-        // weight increase for fast edges and a higher absolute increase for slower edges
-        assertEquals(2 * 144, createWeighting(model).calcEdgeWeight(slow, false), .1);
-        assertEquals(2 * 72, createWeighting(model).calcEdgeWeight(medium, false), .1);
-        assertEquals(2 * 36, createWeighting(model).calcEdgeWeight(fast, false), .1);
-    }
-
-    @Test
-    public void withDistanceInfluence() {
-        BooleanEncodedValue accessEnc = carFE.getAccessEnc();
-        EdgeIteratorState edge = graph.edge(0, 1).setDistance(10_000).set(avSpeedEnc, 50).set(accessEnc, true).setReverse(accessEnc, true);
-        assertEquals(720, createWeighting(new CustomModel().setDistanceInfluence(0)).calcEdgeWeight(edge, false), .1);
-        assertEquals(720_000, createWeighting(new CustomModel().setDistanceInfluence(0)).calcEdgeMillis(edge, false), .1);
-        // distance_influence=30 means that for every kilometer we get additional costs of 30s, so +300s here
-        assertEquals(1020, createWeighting(new CustomModel().setDistanceInfluence(30)).calcEdgeWeight(edge, false), .1);
-        // ... but the travelling time stays the same
-        assertEquals(720_000, createWeighting(new CustomModel().setDistanceInfluence(30)).calcEdgeMillis(edge, false), .1);
-
-        // we can also imagine a shorter but slower road that takes the same time
-        edge = graph.edge(0, 1).setDistance(5_000).set(avSpeedEnc, 25).set(accessEnc, true, true);
-        assertEquals(720, createWeighting(new CustomModel().setDistanceInfluence(0)).calcEdgeWeight(edge, false), .1);
-        assertEquals(720_000, createWeighting(new CustomModel().setDistanceInfluence(0)).calcEdgeMillis(edge, false), .1);
-        // and if we include the distance influence the weight will be bigger but still smaller than what we got for
-        // the longer and faster edge
-        assertEquals(870, createWeighting(new CustomModel().setDistanceInfluence(30)).calcEdgeWeight(edge, false), .1);
-    }
-
-    @Test
-    public void testBasic() {
-        EdgeIteratorState primary = GHUtility.setSpeed(60, true, true, carFE, graph.edge(0, 1).setDistance(10)).
-                set(roadClassEnc, PRIMARY).set(avSpeedEnc, 80);
-        EdgeIteratorState secondary = GHUtility.setSpeed(60, true, true, carFE, graph.edge(1, 2).setDistance(10)).
-                set(roadClassEnc, SECONDARY).set(avSpeedEnc, 70);
-
-        CustomModel vehicleModel = new CustomModel();
-        Map map = new LinkedHashMap();
-
-        map.put(PRIMARY.toString(), 1.0);
-        map.put(CustomWeighting.CATCH_ALL, 0.5);
-        vehicleModel.getPriority().put(KEY, map);
-
-        assertEquals(1.15, createWeighting(vehicleModel).calcEdgeWeight(primary, false), 0.01);
-        assertEquals(1.73, createWeighting(vehicleModel).calcEdgeWeight(secondary, false), 0.01);
-
-        // change priority for primary explicitly and change priority for secondary using catch all
-        map.put(PRIMARY.toString(), 0.7);
-        map.put(CustomWeighting.CATCH_ALL, 0.9);
-        assertEquals(1.34, createWeighting(vehicleModel).calcEdgeWeight(primary, false), 0.01);
-        assertEquals(1.27, createWeighting(vehicleModel).calcEdgeWeight(secondary, false), 0.01);
-
-        // force integer value
-        map.put(PRIMARY.toString(), 1);
-        assertEquals(1.15, createWeighting(vehicleModel).calcEdgeWeight(primary, false), 0.01);
-    }
-
-    @Test
-    public void testMaxSpeedMap() {
-        EdgeIteratorState primary = GHUtility.setSpeed(60, true, true, carFE, graph.edge(0, 1).setDistance(10)).
-                set(roadClassEnc, PRIMARY).set(avSpeedEnc, 80);
-        CustomModel vehicleModel = new CustomModel();
-
-        // without setting a max speed
-        assertEquals(1.15, createWeighting(vehicleModel).calcEdgeWeight(primary, false), 0.01);
-
-        // now reduce speed for road class 'primary' -> the weight increases
-        Map map = new LinkedHashMap();
-        map.put(PRIMARY.toString(), 60);
-        vehicleModel.getMaxSpeed().put(KEY, map);
-        assertEquals(1.3, createWeighting(vehicleModel).calcEdgeWeight(primary, false), 0.01);
-    }
-
-    @Test
-    public void testSpeedFactorBooleanEV() {
-        EdgeIteratorState edge = GHUtility.setSpeed(60, true, true, carFE, graph.edge(0, 1).setDistance(10)).set(avSpeedEnc, 15);
-        CustomModel vehicleModel = new CustomModel();
-        assertEquals(3.1, createWeighting(vehicleModel).calcEdgeWeight(edge, false), 0.01);
-        // here we increase weight for edges that are road class links
-        Map map = new LinkedHashMap<>();
-        map.put("true", 0.5);
-        vehicleModel.getPriority().put(RoadClassLink.KEY, map);
-        CustomWeighting weighting = createWeighting(vehicleModel);
-        BooleanEncodedValue rcLinkEnc = encodingManager.getBooleanEncodedValue(RoadClassLink.KEY);
-        assertEquals(3.1, weighting.calcEdgeWeight(edge.set(rcLinkEnc, false), false), 0.01);
-        assertEquals(5.5, weighting.calcEdgeWeight(edge.set(rcLinkEnc, true), false), 0.01);
-    }
-
-    @Test
-    public void testBoolean() {
-        carFE = new CarFlagEncoder();
-        BooleanEncodedValue specialEnc = new SimpleBooleanEncodedValue("special", true);
-        encodingManager = new EncodingManager.Builder().add(carFE).add(specialEnc).build();
-        avSpeedEnc = carFE.getAverageSpeedEnc();
-        graph = new GraphBuilder(encodingManager).create();
-
-        BooleanEncodedValue accessEnc = carFE.getAccessEnc();
-        EdgeIteratorState edge = graph.edge(0, 1).set(accessEnc, true, true).
-                set(avSpeedEnc, 15).set(specialEnc, false, true).setDistance(10);
-
-        CustomModel vehicleModel = new CustomModel();
-        assertEquals(3.1, createWeighting(vehicleModel).calcEdgeWeight(edge, false), 0.01);
-        Map map = new LinkedHashMap<>();
-        map.put("false", 0.4);
-        map.put("true", 0.8);
-        vehicleModel.getPriority().put("special", map);
-        CustomWeighting weighting = createWeighting(vehicleModel);
-        assertEquals(6.7, weighting.calcEdgeWeight(edge, false), 0.01);
-        assertEquals(3.7, weighting.calcEdgeWeight(edge, true), 0.01);
-    }
-
-    @Test
-    public void testPriority() {
-        CustomModel vehicleModel = new CustomModel();
-
-        Map map = new LinkedHashMap();
-        map.put(MOTORWAY.toString(), 0.1);
-        vehicleModel.getPriority().put(KEY, map);
-        CustomWeighting weighting = createWeighting(vehicleModel);
-
-        EdgeIteratorState edge = GHUtility.setSpeed(60, true, true, carFE, graph.edge(0, 1).setDistance(10)).set(avSpeedEnc, 15);
-        assertEquals(3.1, weighting.calcEdgeWeight(edge, false), 0.01);
-        // we increase weight for motorways
-        edge.set(roadClassEnc, MOTORWAY);
-        assertEquals(24.70, weighting.calcEdgeWeight(edge, false), 0.01);
-        // the edge weight is proportional to the edge distance
-        BooleanEncodedValue accessEnc = carFE.getAccessEnc();
-        edge = graph.edge(0, 1).setDistance(5 * 10).set(avSpeedEnc, 15).set(accessEnc, true).setReverse(accessEnc, true);
-        assertEquals(5 * 3.1, weighting.calcEdgeWeight(edge, false), 0.01);
-        assertEquals(5 * 24.70, weighting.calcEdgeWeight(edge.set(roadClassEnc, MOTORWAY), false), 0.01);
-    }
-
-    @Test
-    public void testAvoidHighSpeed() {
-        CustomWeighting weighting = createWeighting(new CustomModel());
-        EdgeIteratorState slowEdge = GHUtility.setSpeed(60, true, true, carFE, graph.edge(0, 1).setDistance(10)).set(avSpeedEnc, 15).set(maxSpeedEnc, 50);
-        EdgeIteratorState fastEdge = GHUtility.setSpeed(60, true, true, carFE, graph.edge(1, 2).setDistance(10)).set(avSpeedEnc, 60).set(maxSpeedEnc, 70);
-        assertEquals(3.10, weighting.calcEdgeWeight(slowEdge, false), 0.01);
-        assertEquals(1.30, weighting.calcEdgeWeight(fastEdge, false), 0.01);
-
-        Map map = new LinkedHashMap();
-        map.put(">69", 0.2);
-        CustomModel vehicleModel = new CustomModel();
-        vehicleModel.getPriority().put("max_speed", map);
-        weighting = createWeighting(vehicleModel);
-        assertEquals(3.10, weighting.calcEdgeWeight(slowEdge, false), 0.01);
-        assertEquals(3.70, weighting.calcEdgeWeight(fastEdge, false), 0.01);
-
-        // this is currently a bit hidden feature as only the shared encoded values are suggested in the UI
-        map = new LinkedHashMap();
-        map.put(">50", 0.2);
-        vehicleModel = new CustomModel();
-        vehicleModel.getPriority().put(EncodingManager.getKey("car", "average_speed"), map);
-        weighting = createWeighting(vehicleModel);
-        assertEquals(3.10, weighting.calcEdgeWeight(slowEdge, false), 0.01);
-        assertEquals(3.70, weighting.calcEdgeWeight(fastEdge, false), 0.01);
-    }
-
-    @Test
-    public void testIntEncodedValue() {
-        // currently we have no inbuilt encoded value that requires int but it is not bad to have for e.g. lanes
-        CustomWeighting weighting = createWeighting(new CustomModel());
-        EdgeIteratorState slowEdge = GHUtility.setSpeed(60, true, true, carFE, graph.edge(0, 1).setDistance(10)).set(avSpeedEnc, 15).set(laneEnc, 0);
-        EdgeIteratorState fastEdge = GHUtility.setSpeed(60, true, true, carFE, graph.edge(1, 2).setDistance(10)).set(avSpeedEnc, 60).set(laneEnc, 2);
-        assertEquals(3.10, weighting.calcEdgeWeight(slowEdge, false), 0.01);
-        assertEquals(1.30, weighting.calcEdgeWeight(fastEdge, false), 0.01);
-
-        Map map = new LinkedHashMap();
-        map.put(" > 1.5", 0.2); // allow decimal values in range even for int encoded value
-        CustomModel vehicleModel = new CustomModel();
-        vehicleModel.getPriority().put("lanes", map);
-        weighting = createWeighting(vehicleModel);
-        assertEquals(3.10, weighting.calcEdgeWeight(slowEdge, false), 0.01);
-        assertEquals(3.70, weighting.calcEdgeWeight(fastEdge, false), 0.01);
-    }
-
-    @Test
-    public void unorderedMapShouldFail() {
-        Map map = new HashMap<>();
-        map.put(" > 1.5", 0.2); // allow decimal values in range even for int encoded value
-        CustomModel vehicleModel = new CustomModel();
-        vehicleModel.getPriority().put("lanes", map);
-        assertThrows(IllegalArgumentException.class, () -> createWeighting(vehicleModel));
-    }
-
-    @Test
-    public void testAvoidArea() {
-        CustomModel vehicleModel = new CustomModel();
-
-        String areaId = "my_area";
-        vehicleModel.getPriority().put("area_" + areaId, 0.5);
-
-        Coordinate[] coordinates = {new Coordinate(13.722, 51.053), new Coordinate(13.722, 51.055),
-                new Coordinate(13.731, 51.055), new Coordinate(13.731, 51.053), null};
-        coordinates[coordinates.length - 1] = coordinates[0];
-        Geometry poly = new GeometryFactory().createPolygon(coordinates);
-
-        JsonFeature area = new JsonFeature(areaId, "Polygon", null, poly, Collections.<String, Object>emptyMap());
-        vehicleModel.getAreas().put(areaId, area);
-        vehicleModel.setDistanceInfluence(0);
-        CustomWeighting weighting = createWeighting(vehicleModel);
-
-        graph.getNodeAccess().setNode(0, 51.036213, 13.713684);
-        graph.getNodeAccess().setNode(1, 51.036591, 13.719864);
-        // a bit in the north where my_area is:
-        graph.getNodeAccess().setNode(2, 51.054506, 13.723432);
-        graph.getNodeAccess().setNode(3, 51.053589, 13.730679);
-        EdgeIteratorState edge1 = GHUtility.setSpeed(60, true, true, carFE, graph.edge(0, 1).setDistance(500)).set(avSpeedEnc, 15);
-        assertEquals(120, weighting.calcEdgeWeight(edge1, false), 0.01);
-
-        // intersect polygon => increase weight (it doubles, because distance influence is zero)
-        EdgeIteratorState edge2 = GHUtility.setSpeed(60, true, true, carFE, graph.edge(2, 3).setDistance(500)).set(avSpeedEnc, 15);
-        assertTrue(poly.intersects(edge2.fetchWayGeometry(FetchMode.ALL).toLineString(false)));
-        assertEquals(240, weighting.calcEdgeWeight(edge2, false), 0.01);
-    }
-
-    private CustomWeighting createWeighting(CustomModel vehicleModel) {
-        return new CustomWeighting(carFE, encodingManager, NO_TURN_COST_PROVIDER, vehicleModel);
-    }
-}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomModelParserTest.java b/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomModelParserTest.java
new file mode 100644
index 000000000..baac82f32
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomModelParserTest.java
@@ -0,0 +1,176 @@
+/*
+ *  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.weighting.custom;
+
+import com.graphhopper.routing.ev.*;
+import com.graphhopper.routing.util.CarFlagEncoder;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.storage.Graph;
+import com.graphhopper.storage.GraphBuilder;
+import com.graphhopper.storage.GraphHopperStorage;
+import com.graphhopper.util.CustomModel;
+import com.graphhopper.util.EdgeIteratorState;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static com.graphhopper.json.Statement.*;
+import static com.graphhopper.json.Statement.Op.LIMIT;
+import static com.graphhopper.json.Statement.Op.MULTIPLY;
+import static com.graphhopper.routing.ev.RoadClass.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class CustomModelParserTest {
+
+    CarFlagEncoder encoder;
+    Graph graph;
+    EncodingManager encodingManager;
+    EnumEncodedValue<RoadClass> roadClassEnc;
+    DecimalEncodedValue avgSpeedEnc;
+    StringEncodedValue countryEnc;
+
+    @BeforeEach
+    void setup() {
+        encoder = new CarFlagEncoder();
+        countryEnc = new StringEncodedValue("country", 10);
+        encodingManager = new EncodingManager.Builder().add(encoder).add(countryEnc).add(new EnumEncodedValue<>(Surface.KEY, Surface.class)).build();
+        graph = new GraphBuilder(encodingManager).create();
+        avgSpeedEnc = encoder.getAverageSpeedEnc();
+        roadClassEnc = encodingManager.getEnumEncodedValue(RoadClass.KEY, RoadClass.class);
+    }
+
+    @Test
+    void setPriorityForRoadClass() {
+        CustomModel customModel = new CustomModel();
+        customModel.addToPriority(If("road_class == PRIMARY", MULTIPLY, 0.5));
+        CustomWeighting.EdgeToDoubleMapping priorityMapping = CustomModelParser.createWeightingParameters(customModel, encodingManager,
+                encoder.getMaxSpeed(), avgSpeedEnc).getEdgeToPriorityMapping();
+
+        GraphHopperStorage graph = new GraphBuilder(encodingManager).create();
+        EdgeIteratorState edge1 = graph.edge(0, 1).setDistance(100).set(roadClassEnc, RoadClass.PRIMARY);
+        EdgeIteratorState edge2 = graph.edge(1, 2).setDistance(100).set(roadClassEnc, RoadClass.SECONDARY);
+
+        assertEquals(0.5, priorityMapping.get(edge1, false), 1.e-6);
+        assertEquals(1.0, priorityMapping.get(edge2, false), 1.e-6);
+    }
+
+    @Test
+    void testPriority() {
+        EdgeIteratorState primary = graph.edge(0, 1).setDistance(10).
+                set(roadClassEnc, PRIMARY).set(avgSpeedEnc, 80).set(encoder.getAccessEnc(), true, true);
+        EdgeIteratorState secondary = graph.edge(1, 2).setDistance(10).
+                set(roadClassEnc, SECONDARY).set(avgSpeedEnc, 70).set(encoder.getAccessEnc(), true, true);
+        EdgeIteratorState tertiary = graph.edge(1, 2).setDistance(10).
+                set(roadClassEnc, TERTIARY).set(avgSpeedEnc, 70).set(encoder.getAccessEnc(), true, true);
+
+        CustomModel customModel = new CustomModel();
+        customModel.addToPriority(If("road_class == PRIMARY", MULTIPLY, 0.5));
+        customModel.addToPriority(ElseIf("road_class == SECONDARY", MULTIPLY, 0.7));
+        customModel.addToPriority(Else(MULTIPLY, 0.9));
+        customModel.addToPriority(If("road_environment != FERRY", MULTIPLY, 0.8));
+
+        CustomWeighting.EdgeToDoubleMapping priorityMapping = CustomModelParser.createWeightingParameters(customModel, encodingManager,
+                encoder.getMaxSpeed(), avgSpeedEnc).getEdgeToPriorityMapping();
+
+        assertEquals(0.5 * 0.8, priorityMapping.get(primary, false), 0.01);
+        assertEquals(0.7 * 0.8, priorityMapping.get(secondary, false), 0.01);
+        assertEquals(0.9 * 0.8, priorityMapping.get(tertiary, false), 0.01);
+
+        // force integer value
+        customModel = new CustomModel();
+        customModel.addToPriority(If("road_class == PRIMARY", MULTIPLY, 1));
+        customModel.addToPriority(If("road_class == SECONDARY", MULTIPLY, 0.9));
+        priorityMapping = CustomModelParser.createWeightingParameters(customModel, encodingManager,
+                encoder.getMaxSpeed(), avgSpeedEnc).getEdgeToPriorityMapping();
+        assertEquals(1, priorityMapping.get(primary, false), 0.01);
+        assertEquals(0.9, priorityMapping.get(secondary, false), 0.01);
+    }
+
+    @Test
+    public void testSpeedFactorAndPriorityAndMaxSpeed() {
+        EdgeIteratorState primary = graph.edge(0, 1).setDistance(10).
+                set(roadClassEnc, PRIMARY).set(avgSpeedEnc, 80).set(encoder.getAccessEnc(), true, true);
+        EdgeIteratorState secondary = graph.edge(1, 2).setDistance(10).
+                set(roadClassEnc, SECONDARY).set(avgSpeedEnc, 70).set(encoder.getAccessEnc(), true, true);
+
+        CustomModel customModel = new CustomModel();
+        customModel.addToPriority(If("road_class == PRIMARY", MULTIPLY, 0.9));
+        customModel.addToSpeed(If("road_class == PRIMARY", MULTIPLY, 0.8));
+        CustomWeighting.Parameters parameters = CustomModelParser.createWeightingParameters(customModel, encodingManager,
+                encoder.getMaxSpeed(), avgSpeedEnc);
+        assertEquals(0.9, parameters.getEdgeToPriorityMapping().get(primary, false), 0.01);
+        assertEquals(64, parameters.getEdgeToSpeedMapping().get(primary, false), 0.01);
+
+        assertEquals(1, parameters.getEdgeToPriorityMapping().get(secondary, false), 0.01);
+        assertEquals(70, parameters.getEdgeToSpeedMapping().get(secondary, false), 0.01);
+
+        customModel.addToSpeed(If("road_class != PRIMARY", LIMIT, 50));
+        CustomWeighting.EdgeToDoubleMapping speedMapping = CustomModelParser.createWeightingParameters(customModel, encodingManager,
+                encoder.getMaxSpeed(), avgSpeedEnc).getEdgeToSpeedMapping();
+        assertEquals(64, speedMapping.get(primary, false), 0.01);
+        assertEquals(50, speedMapping.get(secondary, false), 0.01);
+    }
+
+    @Test
+    public void testString() {
+        EdgeIteratorState deu = graph.edge(0, 1).setDistance(10).
+                set(countryEnc, "DEU").set(avgSpeedEnc, 80).set(encoder.getAccessEnc(), true, true);
+        EdgeIteratorState blup = graph.edge(1, 2).setDistance(10).
+                set(countryEnc, "blup").set(avgSpeedEnc, 70).set(encoder.getAccessEnc(), true, true);
+
+        CustomModel customModel = new CustomModel();
+        customModel.addToPriority(If("country == \"DEU\"", MULTIPLY, 0.9));
+        customModel.addToPriority(ElseIf("country == \"blup\"", MULTIPLY, 0.7));
+        customModel.addToPriority(Else(MULTIPLY, 0.5));
+        CustomWeighting.EdgeToDoubleMapping priorityMapping = CustomModelParser.createWeightingParameters(customModel, encodingManager,
+                encoder.getMaxSpeed(), avgSpeedEnc).getEdgeToPriorityMapping();
+        assertEquals(0.9, priorityMapping.get(deu, false), 0.01);
+        assertEquals(0.7, priorityMapping.get(blup, false), 0.01);
+    }
+
+    @Test
+    void testIllegalOrder() {
+        CustomModel customModel = new CustomModel();
+        customModel.addToPriority(Else(MULTIPLY, 0.9));
+        customModel.addToPriority(If("road_environment != FERRY", MULTIPLY, 0.8));
+        assertThrows(IllegalArgumentException.class, () -> CustomModelParser.createWeightingParameters(customModel, encodingManager,
+                encoder.getMaxSpeed(), avgSpeedEnc));
+
+        CustomModel customModel2 = new CustomModel();
+        customModel2.addToPriority(ElseIf("road_environment != FERRY", MULTIPLY, 0.9));
+        customModel2.addToPriority(If("road_class != PRIMARY", MULTIPLY, 0.8));
+        assertThrows(IllegalArgumentException.class, () -> CustomModelParser.createWeightingParameters(customModel2, encodingManager,
+                encoder.getMaxSpeed(), avgSpeedEnc));
+    }
+
+    @Test
+    public void testFindMaxSpeed() {
+        CustomModel customModel = new CustomModel();
+        customModel.addToSpeed(If("true", LIMIT, 100));
+        assertEquals(100, CustomModelParser.findMaxSpeed(customModel, 120));
+
+        customModel.addToSpeed(Else(LIMIT, 20));
+        assertEquals(100, CustomModelParser.findMaxSpeed(customModel, 120));
+
+        customModel = new CustomModel();
+        customModel.addToSpeed(If("road_environment == BRIDGE", LIMIT, 85));
+        customModel.addToSpeed(Else(LIMIT, 100));
+        assertEquals(100, CustomModelParser.findMaxSpeed(customModel, 120));
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomModelTest.java b/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomModelTest.java
new file mode 100644
index 000000000..5f9bfefa5
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomModelTest.java
@@ -0,0 +1,87 @@
+/*
+ *  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.weighting.custom;
+
+import com.graphhopper.json.Statement;
+import com.graphhopper.util.CustomModel;
+import org.junit.jupiter.api.Test;
+
+import java.util.Iterator;
+
+import static com.graphhopper.json.Statement.ElseIf;
+import static com.graphhopper.json.Statement.If;
+import static com.graphhopper.json.Statement.Op.MULTIPLY;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class CustomModelTest {
+
+    @Test
+    public void testTooBigFactor() {
+        CustomModel truck = new CustomModel();
+        truck.addToPriority(If("max_width < 3", MULTIPLY, 10));
+        // it is ok server-side CustomModel
+        assertEquals(1, CustomModel.merge(truck, new CustomModel()).getPriority().size());
+        // but not for query CustomModel
+        assertThrows(IllegalArgumentException.class, () -> CustomModel.merge(new CustomModel(), truck));
+    }
+
+    @Test
+    public void testMergeComparisonKeys() {
+        CustomModel truck = new CustomModel();
+        truck.addToPriority(If("max_width < 3", MULTIPLY, 0));
+        CustomModel car = new CustomModel();
+        car.addToPriority(If("max_width<2", MULTIPLY, 0));
+        CustomModel bike = new CustomModel();
+        bike.addToPriority(If("max_weight<0.02", MULTIPLY, 0));
+
+        assertEquals(2, CustomModel.merge(bike, car).getPriority().size());
+        assertEquals(1, bike.getPriority().size());
+        assertEquals(1, car.getPriority().size());
+    }
+
+    @Test
+    public void testMergeElse() {
+        CustomModel truck = new CustomModel();
+        truck.addToPriority(If("max_width < 3", MULTIPLY, 0));
+
+        CustomModel car = new CustomModel();
+        car.addToPriority(If("max_width < 2", MULTIPLY, 0));
+
+        CustomModel merged = CustomModel.merge(truck, car);
+        assertEquals(2, merged.getPriority().size());
+        assertEquals(1, car.getPriority().size());
+    }
+
+    @Test
+    public void testMergeEmptyModel() {
+        CustomModel emptyCar = new CustomModel();
+        CustomModel car = new CustomModel();
+        car.addToPriority(If("road_class==primary", MULTIPLY, 0.5));
+        car.addToPriority(ElseIf("road_class==tertiary", MULTIPLY, 0.8));
+
+        Iterator<Statement> iter = CustomModel.merge(emptyCar, car).getPriority().iterator();
+        assertEquals(0.5, iter.next().getValue());
+        assertEquals(0.8, iter.next().getValue());
+
+        iter = CustomModel.merge(car, emptyCar).getPriority().iterator();
+        assertEquals(0.5, iter.next().getValue());
+        assertEquals(0.8, iter.next().getValue());
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomWeightingTest.java b/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomWeightingTest.java
new file mode 100644
index 000000000..389c7fa97
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomWeightingTest.java
@@ -0,0 +1,257 @@
+package com.graphhopper.routing.weighting.custom;
+
+import com.bedatadriven.jackson.datatype.jts.JtsModule;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.graphhopper.routing.ev.*;
+import com.graphhopper.routing.util.CarFlagEncoder;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.routing.util.FlagEncoder;
+import com.graphhopper.routing.util.parsers.OSMBikeNetworkTagParser;
+import com.graphhopper.routing.util.parsers.OSMHazmatParser;
+import com.graphhopper.routing.util.parsers.OSMTollParser;
+import com.graphhopper.routing.weighting.FastestWeighting;
+import com.graphhopper.routing.weighting.Weighting;
+import com.graphhopper.storage.GraphBuilder;
+import com.graphhopper.storage.GraphHopperStorage;
+import com.graphhopper.util.CustomModel;
+import com.graphhopper.util.EdgeIteratorState;
+import com.graphhopper.util.GHUtility;
+import com.graphhopper.util.JsonFeature;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static com.graphhopper.json.Statement.*;
+import static com.graphhopper.json.Statement.Op.MULTIPLY;
+import static com.graphhopper.routing.ev.RoadClass.*;
+import static com.graphhopper.routing.weighting.TurnCostProvider.NO_TURN_COST_PROVIDER;
+import static org.junit.jupiter.api.Assertions.*;
+
+class CustomWeightingTest {
+
+    GraphHopperStorage graph;
+    DecimalEncodedValue avSpeedEnc;
+    BooleanEncodedValue accessEnc;
+    DecimalEncodedValue maxSpeedEnc;
+    EnumEncodedValue<RoadClass> roadClassEnc;
+    EncodingManager encodingManager;
+    FlagEncoder carFE;
+
+    @BeforeEach
+    public void setup() {
+        carFE = new CarFlagEncoder().setSpeedTwoDirections(true);
+        encodingManager = new EncodingManager.Builder().add(carFE).add(new OSMTollParser()).add(new OSMHazmatParser()).add(new OSMBikeNetworkTagParser()).build();
+        avSpeedEnc = carFE.getAverageSpeedEnc();
+        accessEnc = carFE.getAccessEnc();
+        maxSpeedEnc = encodingManager.getDecimalEncodedValue(MaxSpeed.KEY);
+        roadClassEnc = encodingManager.getEnumEncodedValue(KEY, RoadClass.class);
+        graph = new GraphBuilder(encodingManager).create();
+    }
+
+    @Test
+    public void speedOnly() {
+        // 50km/h -> 72s per km, 100km/h -> 36s per km
+        EdgeIteratorState edge;
+        GHUtility.setSpeed(50, 100, carFE, edge = graph.edge(0, 1).setDistance(1000));
+        assertEquals(72, createWeighting(new CustomModel().setDistanceInfluence(0)).calcEdgeWeight(edge, false), 1.e-6);
+        assertEquals(36, createWeighting(new CustomModel().setDistanceInfluence(0)).calcEdgeWeight(edge, true), 1.e-6);
+    }
+
+    @Test
+    public void withPriority() {
+        // 25km/h -> 144s per km, 50km/h -> 72s per km, 100km/h -> 36s per km
+        EdgeIteratorState slow = GHUtility.setSpeed(25, true, true, carFE, graph.edge(0, 1).setDistance(1000)).
+                set(roadClassEnc, SECONDARY);
+        EdgeIteratorState medium = GHUtility.setSpeed(50, true, true, carFE, graph.edge(0, 1).setDistance(1000)).
+                set(roadClassEnc, SECONDARY);
+        EdgeIteratorState fast = GHUtility.setSpeed(100, true, true, carFE, graph.edge(0, 1).setDistance(1000)).
+                set(roadClassEnc, SECONDARY);
+
+        // without priority costs fastest weighting is the same as custom weighting
+        assertEquals(144, new FastestWeighting(carFE, NO_TURN_COST_PROVIDER).calcEdgeWeight(slow, false), .1);
+        assertEquals(72, new FastestWeighting(carFE, NO_TURN_COST_PROVIDER).calcEdgeWeight(medium, false), .1);
+        assertEquals(36, new FastestWeighting(carFE, NO_TURN_COST_PROVIDER).calcEdgeWeight(fast, false), .1);
+
+        CustomModel model = new CustomModel().setDistanceInfluence(0);
+        assertEquals(144, createWeighting(model).calcEdgeWeight(slow, false), .1);
+        assertEquals(72, createWeighting(model).calcEdgeWeight(medium, false), .1);
+        assertEquals(36, createWeighting(model).calcEdgeWeight(fast, false), .1);
+
+        // if we reduce the priority we get higher edge weights
+        model.addToPriority(If("road_class == SECONDARY", MULTIPLY, 0.5));
+        // the absolute priority costs depend on the speed, so setting priority=0.5 means a lower absolute weight
+        // weight increase for fast edges and a higher absolute increase for slower edges
+        assertEquals(2 * 144, createWeighting(model).calcEdgeWeight(slow, false), .1);
+        assertEquals(2 * 72, createWeighting(model).calcEdgeWeight(medium, false), .1);
+        assertEquals(2 * 36, createWeighting(model).calcEdgeWeight(fast, false), .1);
+    }
+
+    @Test
+    public void withDistanceInfluence() {
+        BooleanEncodedValue accessEnc = carFE.getAccessEnc();
+        EdgeIteratorState edge = graph.edge(0, 1).setDistance(10_000).set(avSpeedEnc, 50).set(accessEnc, true, true);
+        assertEquals(720, createWeighting(new CustomModel().setDistanceInfluence(0)).calcEdgeWeight(edge, false), .1);
+        assertEquals(720_000, createWeighting(new CustomModel().setDistanceInfluence(0)).calcEdgeMillis(edge, false), .1);
+        // distance_influence=30 means that for every kilometer we get additional costs of 30s, so +300s here
+        assertEquals(1020, createWeighting(new CustomModel().setDistanceInfluence(30)).calcEdgeWeight(edge, false), .1);
+        // ... but the travelling time stays the same
+        assertEquals(720_000, createWeighting(new CustomModel().setDistanceInfluence(30)).calcEdgeMillis(edge, false), .1);
+
+        // we can also imagine a shorter but slower road that takes the same time
+        edge = graph.edge(0, 1).setDistance(5_000).set(avSpeedEnc, 25).set(accessEnc, true, true);
+        assertEquals(720, createWeighting(new CustomModel().setDistanceInfluence(0)).calcEdgeWeight(edge, false), .1);
+        assertEquals(720_000, createWeighting(new CustomModel().setDistanceInfluence(0)).calcEdgeMillis(edge, false), .1);
+        // and if we include the distance influence the weight will be bigger but still smaller than what we got for
+        // the longer and faster edge
+        assertEquals(870, createWeighting(new CustomModel().setDistanceInfluence(30)).calcEdgeWeight(edge, false), .1);
+    }
+
+    @Test
+    public void testSpeedFactorBooleanEV() {
+        EdgeIteratorState edge = GHUtility.setSpeed(15, true, true, carFE, graph.edge(0, 1).setDistance(10));
+        CustomModel vehicleModel = new CustomModel();
+        assertEquals(3.1, createWeighting(vehicleModel).calcEdgeWeight(edge, false), 0.01);
+        // here we increase weight for edges that are road class links
+        vehicleModel.addToPriority(If(RoadClassLink.KEY, MULTIPLY, 0.5));
+        Weighting weighting = createWeighting(vehicleModel);
+        BooleanEncodedValue rcLinkEnc = encodingManager.getBooleanEncodedValue(RoadClassLink.KEY);
+        assertEquals(3.1, weighting.calcEdgeWeight(edge.set(rcLinkEnc, false), false), 0.01);
+        assertEquals(5.5, weighting.calcEdgeWeight(edge.set(rcLinkEnc, true), false), 0.01);
+    }
+
+    @Test
+    public void testBoolean() {
+        carFE = new CarFlagEncoder();
+        BooleanEncodedValue specialEnc = new SimpleBooleanEncodedValue("special", true);
+        encodingManager = new EncodingManager.Builder().add(carFE).add(specialEnc).build();
+        avSpeedEnc = carFE.getAverageSpeedEnc();
+        graph = new GraphBuilder(encodingManager).create();
+
+        BooleanEncodedValue accessEnc = carFE.getAccessEnc();
+        EdgeIteratorState edge = graph.edge(0, 1).set(accessEnc, true).setReverse(accessEnc, true).
+                set(avSpeedEnc, 15).set(specialEnc, false).setReverse(specialEnc, true).setDistance(10);
+
+        CustomModel vehicleModel = new CustomModel();
+        assertEquals(3.1, createWeighting(vehicleModel).calcEdgeWeight(edge, false), 0.01);
+        vehicleModel.addToPriority(If("special == true", MULTIPLY, 0.8));
+        vehicleModel.addToPriority(If("special == false", MULTIPLY, 0.4));
+        Weighting weighting = createWeighting(vehicleModel);
+        assertEquals(6.7, weighting.calcEdgeWeight(edge, false), 0.01);
+        assertEquals(3.7, weighting.calcEdgeWeight(edge, true), 0.01);
+    }
+
+    @Test
+    public void testSpeedFactorAndPriority() {
+        EdgeIteratorState primary = graph.edge(0, 1).setDistance(10).
+                set(roadClassEnc, PRIMARY).set(avSpeedEnc, 80).set(accessEnc, true, true);
+        EdgeIteratorState secondary = graph.edge(1, 2).setDistance(10).
+                set(roadClassEnc, SECONDARY).set(avSpeedEnc, 70).set(accessEnc, true, true);
+
+        CustomModel vehicleModel = new CustomModel();
+        vehicleModel.addToPriority(If("road_class != PRIMARY", MULTIPLY, 0.5));
+        vehicleModel.addToSpeed(If("road_class != PRIMARY", MULTIPLY, 0.9));
+        assertEquals(1.15, createWeighting(vehicleModel).calcEdgeWeight(primary, false), 0.01);
+        assertEquals(1.84, createWeighting(vehicleModel).calcEdgeWeight(secondary, false), 0.01);
+
+        vehicleModel = new CustomModel();
+        vehicleModel.addToPriority(If("road_class == PRIMARY", MULTIPLY, 1.0));
+        vehicleModel.addToPriority(Else(MULTIPLY, 0.5));
+        vehicleModel.addToSpeed(If("road_class != PRIMARY", MULTIPLY, 0.9));
+        assertEquals(1.15, createWeighting(vehicleModel).calcEdgeWeight(primary, false), 0.01);
+        assertEquals(1.84, createWeighting(vehicleModel).calcEdgeWeight(secondary, false), 0.01);
+    }
+
+    @Test
+    public void testIssueSameKey() {
+        EdgeIteratorState withToll = graph.edge(0, 1).setDistance(10).
+                set(avSpeedEnc, 80).set(accessEnc, true, true);
+        EdgeIteratorState noToll = graph.edge(1, 2).setDistance(10).
+                set(avSpeedEnc, 80).set(accessEnc, true, true);
+
+        CustomModel vehicleModel = new CustomModel();
+        vehicleModel.addToSpeed(If("toll != NO", MULTIPLY, 0.8));
+        vehicleModel.addToSpeed(If("hazmat != NO", MULTIPLY, 0.8));
+        assertEquals(1.26, createWeighting(vehicleModel).calcEdgeWeight(withToll, false), 0.01);
+        assertEquals(1.26, createWeighting(vehicleModel).calcEdgeWeight(noToll, false), 0.01);
+
+        vehicleModel = new CustomModel();
+        vehicleModel.addToSpeed(If("bike_network != OTHER", MULTIPLY, 0.8));
+        assertEquals(1.26, createWeighting(vehicleModel).calcEdgeWeight(withToll, false), 0.01);
+        assertEquals(1.26, createWeighting(vehicleModel).calcEdgeWeight(noToll, false), 0.01);
+    }
+
+    @Test
+    public void testFirstMatch() {
+        EdgeIteratorState primary = graph.edge(0, 1).setDistance(10).
+                set(roadClassEnc, PRIMARY).set(avSpeedEnc, 80).set(accessEnc, true, true);
+        EdgeIteratorState secondary = graph.edge(1, 2).setDistance(10).
+                set(roadClassEnc, SECONDARY).set(avSpeedEnc, 70).set(accessEnc, true, true);
+
+        CustomModel vehicleModel = new CustomModel();
+        vehicleModel.addToSpeed(If("road_class == PRIMARY", MULTIPLY, 0.8));
+        assertEquals(1.26, createWeighting(vehicleModel).calcEdgeWeight(primary, false), 0.01);
+        assertEquals(1.21, createWeighting(vehicleModel).calcEdgeWeight(secondary, false), 0.01);
+
+        vehicleModel.addToPriority(If("road_class == PRIMARY", MULTIPLY, 0.9));
+        vehicleModel.addToPriority(ElseIf("road_class == SECONDARY", MULTIPLY, 0.8));
+
+        assertEquals(1.33, createWeighting(vehicleModel).calcEdgeWeight(primary, false), 0.01);
+        assertEquals(1.34, createWeighting(vehicleModel).calcEdgeWeight(secondary, false), 0.01);
+    }
+
+    @Test
+    public void testCarAccess() {
+        EdgeIteratorState edge40 = graph.edge(0, 1).setDistance(10).set(avSpeedEnc, 40).set(accessEnc, true, true);
+        EdgeIteratorState edge50 = graph.edge(1, 2).setDistance(10).set(avSpeedEnc, 50).set(accessEnc, true, true);
+
+        CustomModel vehicleModel = new CustomModel();
+        vehicleModel.addToPriority(If("car$average_speed > 40", MULTIPLY, 0.5));
+
+        assertEquals(1.60, createWeighting(vehicleModel).calcEdgeWeight(edge40, false), 0.01);
+        assertEquals(2.14, createWeighting(vehicleModel).calcEdgeWeight(edge50, false), 0.01);
+    }
+
+    @Test
+    public void testArea() throws Exception {
+        EdgeIteratorState primary = graph.edge(0, 1).setDistance(10).
+                set(roadClassEnc, PRIMARY).set(avSpeedEnc, 80).set(accessEnc, true, true);
+        EdgeIteratorState secondary = graph.edge(1, 2).setDistance(10).
+                set(roadClassEnc, SECONDARY).set(avSpeedEnc, 70).set(accessEnc, true, true);
+
+        CustomModel vehicleModel = new CustomModel();
+        vehicleModel.addToPriority(If("road_class == PRIMARY", MULTIPLY, 1.0));
+        vehicleModel.addToPriority(If("in_area_custom1", MULTIPLY, 0.5));
+
+        ObjectMapper om = new ObjectMapper().registerModule(new JtsModule());
+        JsonFeature json = om.readValue("{ \"geometry\":{ \"type\": \"Polygon\", \"coordinates\": " +
+                "[[[11.5818,50.0126], [11.5818,50.0119], [11.5861,50.0119], [11.5861,50.0126], [11.5818,50.0126]]] }}", JsonFeature.class);
+        vehicleModel.getAreas().put("custom1", json);
+
+        assertEquals(1.15, createWeighting(vehicleModel).calcEdgeWeight(primary, false), 0.01);
+        assertEquals(1.21, createWeighting(vehicleModel).calcEdgeWeight(secondary, false), 0.01);
+    }
+
+    @Test
+    public void testMaxSpeedFallBack() {
+        assertEquals(140, carFE.getMaxSpeed(), 0.1);
+        String message = assertThrows(IllegalArgumentException.class, () -> createWeighting(new CustomModel().
+                addToSpeed(If("true", Op.LIMIT, 150)))).
+                getMessage();
+        assertEquals("Can never apply 'limit to': 150.0 because maximum vehicle speed is 140.0", message);
+        assertEquals(50 + 30, createWeighting(new CustomModel().
+                addToSpeed(If("true", Op.LIMIT, 72)).setDistanceInfluence(30)).getMinWeight(1000));
+    }
+
+    @Test
+    public void tooLarge() {
+        CustomModel customModel = new CustomModel();
+        for (int i = 0; i < 1050; i++) {
+            customModel.addToPriority(If("road_class == MOTORWAY || road_class == SECONDARY || road_class == PRIMARY", MULTIPLY, 0.1));
+        }
+        IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> createWeighting(customModel));
+        assertTrue(ex.getMessage().startsWith("Custom Model too big"), ex.getMessage());
+    }
+
+    private Weighting createWeighting(CustomModel vehicleModel) {
+        return CustomModelParser.createWeighting(carFE, encodingManager, NO_TURN_COST_PROVIDER, vehicleModel);
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/weighting/custom/DecimalToValueEntryTest.java b/core/src/test/java/com/graphhopper/routing/weighting/custom/DecimalToValueEntryTest.java
deleted file mode 100644
index 78540d6c9..000000000
--- a/core/src/test/java/com/graphhopper/routing/weighting/custom/DecimalToValueEntryTest.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.graphhopper.routing.weighting.custom;
-
-import com.graphhopper.routing.ev.DecimalEncodedValue;
-import com.graphhopper.routing.ev.EncodedValue;
-import com.graphhopper.routing.ev.UnsignedDecimalEncodedValue;
-import com.graphhopper.storage.IntsRef;
-import com.graphhopper.util.GHUtility;
-import org.junit.jupiter.api.Test;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-class DecimalToValueEntryTest {
-
-    @Test
-    public void testMapParsing() {
-        EncodedValue.InitializerConfig config = new EncodedValue.InitializerConfig();
-        DecimalEncodedValue decimalEncodedValue = new UnsignedDecimalEncodedValue("my_speed", 7, 0.5, false);
-        decimalEncodedValue.init(config);
-
-        double defaultValue = 33;
-        Map<String, Object> map = new HashMap<>();
-        map.put(">8", 24);
-        EdgeToValueEntry entry = DecimalToValueEntry.create("priority.my_speed", decimalEncodedValue, map, defaultValue, 1, 100);
-        IntsRef flags = new IntsRef(1);
-
-        assertValue(defaultValue, 0, flags, decimalEncodedValue, entry);
-        assertValue(defaultValue, 8.0, flags, decimalEncodedValue, entry);
-        assertValue(24.0, 10, flags, decimalEncodedValue, entry);
-    }
-
-    static void assertValue(double expectedReturnValue, double returnedFromEdge, IntsRef flags, DecimalEncodedValue decimalEncodedValue, EdgeToValueEntry entry) {
-        decimalEncodedValue.setDecimal(false, flags, returnedFromEdge);
-        assertEquals(expectedReturnValue, entry.getValue(GHUtility.createMockedEdgeIteratorState(100, flags), false));
-    }
-}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/weighting/custom/ExpressionVisitorTest.java b/core/src/test/java/com/graphhopper/routing/weighting/custom/ExpressionVisitorTest.java
new file mode 100644
index 000000000..5fbfb4648
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/weighting/custom/ExpressionVisitorTest.java
@@ -0,0 +1,159 @@
+package com.graphhopper.routing.weighting.custom;
+
+import com.graphhopper.json.Statement;
+import com.graphhopper.routing.ev.EncodedValueLookup;
+import com.graphhopper.routing.ev.StringEncodedValue;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.storage.GraphBuilder;
+import com.graphhopper.storage.IntsRef;
+import com.graphhopper.util.Helper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+import static com.graphhopper.json.Statement.If;
+import static com.graphhopper.routing.weighting.custom.CustomModelParser.isValidVariableName;
+import static com.graphhopper.routing.weighting.custom.ExpressionVisitor.parseExpression;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ExpressionVisitorTest {
+
+    private EncodedValueLookup lookup;
+
+    @BeforeEach
+    public void before() {
+        StringEncodedValue sev = new StringEncodedValue("country", 10);
+        lookup = new GraphBuilder(new EncodingManager.Builder().add(sev).build()).create().getEncodingManager();
+        sev.setString(false, new IntsRef(1), "DEU");
+    }
+
+    @Test
+    public void protectUsFromStuff() {
+        ExpressionVisitor.NameValidator allNamesInvalid = s -> false;
+        for (String toParse : Arrays.asList("",
+                "new Object()",
+                "java.lang.Object",
+                "Test.class",
+                "new Object(){}.toString().length",
+                "{ 5}",
+                "{ 5, 7 }",
+                "Object.class",
+                "System.out.println(\"\")",
+                "something.newInstance()",
+                "e.getClass ( )",
+                "edge.getDistance()*7/*test",
+                "edge.getDistance()//*test",
+                "edge . getClass()",
+                "(edge = edge) == edge",
+                ") edge (",
+                "in(area_blup(), edge)")) {
+            ExpressionVisitor.ParseResult res = parseExpression(toParse, allNamesInvalid, lookup);
+            assertFalse(res.ok, "should not be simple condition: " + toParse);
+            assertTrue(res.guessedVariables == null || res.guessedVariables.isEmpty());
+        }
+
+        assertFalse(parseExpression("edge; getClass()", allNamesInvalid, lookup).ok);
+    }
+
+    @Test
+    public void testConvertExpression() {
+        ExpressionVisitor.NameValidator validVariable = s -> isValidVariableName(s)
+                || Helper.toUpperCase(s).equals(s) || s.equals("road_class") || s.equals("toll");
+
+        ExpressionVisitor.ParseResult result = parseExpression("toll == NO", validVariable, lookup);
+        assertTrue(result.ok);
+        assertEquals("[toll]", result.guessedVariables.toString());
+
+        assertEquals("road_class == RoadClass.PRIMARY",
+                parseExpression("road_class == PRIMARY", validVariable, lookup).converted.toString());
+        assertEquals("toll == Toll.NO", parseExpression("toll == NO", validVariable, lookup).converted.toString());
+        assertEquals("toll == Toll.NO || road_class == RoadClass.NO", parseExpression("toll == NO || road_class == NO", validVariable, lookup).converted.toString());
+
+        // convert in_area variable to function call:
+        assertEquals(CustomWeightingHelper.class.getSimpleName() + ".in(this.in_area_custom_1, edge)",
+                parseExpression("in_area_custom_1", validVariable, lookup).converted.toString());
+
+        // no need to inject:
+        assertNull(parseExpression("toll == Toll.NO", validVariable, lookup).converted);
+    }
+
+    @Test
+    public void testStringExpression() {
+        ExpressionVisitor.NameValidator validVariable = s -> isValidVariableName(s) || s.equals("country");
+
+        ExpressionVisitor.ParseResult result = parseExpression("country == \"DEU\"", validVariable, lookup);
+        assertTrue(result.ok);
+        assertEquals("[country]", result.guessedVariables.toString());
+        assertEquals("country == 1", result.converted.toString());
+
+        // unknown String should result in a negative integer. If we would throw an Exception here the same script that
+        // works on a global map will not work on a smaller map where the "blup" String is missing
+        result = parseExpression("country == \"blup\"", validVariable, lookup);
+        assertTrue(result.ok);
+        assertEquals("[country]", result.guessedVariables.toString());
+        assertEquals("country == -1", result.converted.toString());
+    }
+
+    @Test
+    public void isValidAndSimpleCondition() {
+        ExpressionVisitor.NameValidator validVariable = s -> isValidVariableName(s)
+                || Helper.toUpperCase(s).equals(s) || s.equals("road_class") || s.equals("toll");
+        ExpressionVisitor.ParseResult result = parseExpression("edge == edge", validVariable, lookup);
+        assertTrue(result.ok);
+        assertEquals("[edge]", result.guessedVariables.toString());
+
+        result = parseExpression("Math.sqrt(2)", validVariable, lookup);
+        assertTrue(result.ok);
+        assertTrue(result.guessedVariables.isEmpty());
+
+        result = parseExpression("edge.blup()", validVariable, lookup);
+        assertFalse(result.ok);
+        assertTrue(result.guessedVariables.isEmpty());
+
+        result = parseExpression("edge.getDistance()", validVariable, lookup);
+        assertTrue(result.ok);
+        assertEquals("[edge]", result.guessedVariables.toString());
+        assertFalse(parseExpression("road_class == PRIMARY", s -> false, lookup).ok);
+        result = parseExpression("road_class == PRIMARY", validVariable, lookup);
+        assertTrue(result.ok);
+        assertEquals("[road_class]", result.guessedVariables.toString());
+
+        result = parseExpression("toll == Toll.NO", validVariable, lookup);
+        assertFalse(result.ok);
+        assertEquals("[toll]", result.guessedVariables.toString());
+
+        assertTrue(parseExpression("road_class.ordinal()*2 == PRIMARY.ordinal()*2", validVariable, lookup).ok);
+        assertTrue(parseExpression("Math.sqrt(road_class.ordinal()) > 1", validVariable, lookup).ok);
+    }
+
+    @Test
+    public void errorMessage() {
+        ExpressionVisitor.NameValidator validVariable = s -> lookup.hasEncodedValue(s);
+
+        // existing encoded value but not added
+        IllegalArgumentException ret = assertThrows(IllegalArgumentException.class,
+                () -> ExpressionVisitor.parseExpressions(new StringBuilder(),
+                        validVariable, "[HERE]", new HashSet<>(),
+                        Arrays.asList(If("max_weight > 10", Statement.Op.MULTIPLY, 0)),
+                        lookup, ""));
+        assertTrue(ret.getMessage().startsWith("[HERE] invalid expression \"max_weight > 10\": encoded value 'max_weight' not available"), ret.getMessage());
+
+        // invalid variable or constant (NameValidator returns false)
+        ret = assertThrows(IllegalArgumentException.class,
+                () -> ExpressionVisitor.parseExpressions(new StringBuilder(),
+                        validVariable, "[HERE]", new HashSet<>(),
+                        Arrays.asList(If("country == GERMANY", Statement.Op.MULTIPLY, 0)),
+                        lookup, ""));
+        assertTrue(ret.getMessage().startsWith("[HERE] invalid expression \"country == GERMANY\": identifier GERMANY invalid"), ret.getMessage());
+
+        // not whitelisted method
+        ret = assertThrows(IllegalArgumentException.class,
+                () -> ExpressionVisitor.parseExpressions(new StringBuilder(),
+                        validVariable, "[HERE]", new HashSet<>(),
+                        Arrays.asList(If("edge.fetchWayGeometry().size() > 2", Statement.Op.MULTIPLY, 0)),
+                        lookup, ""));
+        assertTrue(ret.getMessage().startsWith("[HERE] invalid expression \"edge.fetchWayGeometry().size() > 2\": size is illegal method"), ret.getMessage());
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/weighting/custom/PriorityCalculatorTest.java b/core/src/test/java/com/graphhopper/routing/weighting/custom/PriorityCalculatorTest.java
deleted file mode 100644
index dffdebf20..000000000
--- a/core/src/test/java/com/graphhopper/routing/weighting/custom/PriorityCalculatorTest.java
+++ /dev/null
@@ -1,127 +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.weighting.custom;
-
-import com.graphhopper.routing.ev.*;
-import com.graphhopper.routing.util.CarFlagEncoder;
-import com.graphhopper.util.CustomModel;
-import com.graphhopper.routing.util.EncodingManager;
-import com.graphhopper.routing.util.FlagEncoder;
-import com.graphhopper.storage.GraphBuilder;
-import com.graphhopper.util.EdgeIteratorState;
-import com.graphhopper.util.GHUtility;
-import org.junit.jupiter.api.Test;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-class PriorityCalculatorTest {
-
-    private final EncodingManager em;
-    private final EdgeIteratorState edge;
-
-    PriorityCalculatorTest() {
-        // in this test we use the same edge for every test and check the calculated priority for different custom models
-        // and depending on additional properties of the edge we set in the tests
-        FlagEncoder encoder = new CarFlagEncoder();
-        em = EncodingManager.create(encoder);
-        edge = GHUtility.setSpeed(60, true, true, encoder, new GraphBuilder(em).create().edge(0, 1).setDistance(1000));
-    }
-
-    @Test
-    public void priority() {
-        CustomModel model = new CustomModel();
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put("*", 0.3);
-        model.getPriority().put(RoadClass.KEY, map);
-        assertEquals(0.3, calcPriority(edge, model));
-        // our edge might or might not be affected
-        EnumEncodedValue<RoadClass> roadClass = em.getEnumEncodedValue(RoadClass.KEY, RoadClass.class);
-        edge.set(roadClass, RoadClass.PRIMARY);
-        model.getPriority().clear();
-        assertEquals(1.0, calcPriority(edge, new CustomModel()));
-        map.clear();
-        map.put(RoadClass.PRIMARY.toString(), 0.7);
-        model.getPriority().put(RoadClass.KEY, map);
-        assertEquals(0.7, calcPriority(edge, model));
-    }
-
-    @Test
-    public void invalidPriority() {
-        CustomModel model = new CustomModel();
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put("*", 1.1);
-        model.getPriority().put(RoadClass.KEY, map);
-        try {
-            assertEquals(1.0, calcPriority(edge, model));
-            fail();
-        } catch (IllegalArgumentException e) {
-            assertTrue(e.getMessage().contains("priority.road_class cannot be bigger than 1.0, was 1.1"), e.getMessage());
-        }
-        // negative values are not allowed
-        try {
-            map.put("*", -5);
-            assertEquals(1.0, calcPriority(edge, model));
-            fail();
-        } catch (IllegalArgumentException e) {
-            assertTrue(e.getMessage().contains("priority.road_class cannot be smaller than 0.0, was -5.0"), e.getMessage());
-        }
-    }
-
-    @Test
-    public void priorityFactorMultiple() {
-        EnumEncodedValue<RoadClass> roadClass = em.getEnumEncodedValue(RoadClass.KEY, RoadClass.class);
-        EnumEncodedValue<RoadEnvironment> roadEnvironment = em.getEnumEncodedValue(RoadEnvironment.KEY, RoadEnvironment.class);
-        edge.set(roadClass, RoadClass.PRIMARY);
-        edge.set(roadEnvironment, RoadEnvironment.BRIDGE);
-        Map<String, Object> roadClassMap = new LinkedHashMap<>();
-        roadClassMap.put(RoadClass.PRIMARY.toString(), 0.7);
-        Map<String, Object> roadEnvironmentMap = new LinkedHashMap<>();
-        roadEnvironmentMap.put(RoadEnvironment.BRIDGE.toString(), 0.5);
-        CustomModel model = new CustomModel();
-        model.getPriority().put(RoadClass.KEY, roadClassMap);
-        model.getPriority().put(RoadEnvironment.KEY, roadEnvironmentMap);
-        assertEquals(0.35, calcPriority(edge, model));
-    }
-
-    @Test
-    public void directionDependent() {
-        EnumEncodedValue<RoadClass> roadClass = em.getEnumEncodedValue(RoadClass.KEY, RoadClass.class);
-        DecimalEncodedValue maxSpeedEnc = em.getDecimalEncodedValue(MaxSpeed.KEY);
-        edge.set(roadClass, RoadClass.PRIMARY);
-        edge.set(maxSpeedEnc, 110, 50);
-
-        Map<String, Object> maxSpeedMap = new LinkedHashMap<>();
-        maxSpeedMap.put("<100", 0.5);
-
-        CustomModel model = new CustomModel();
-        model.getPriority().put(MaxSpeed.KEY, maxSpeedMap);
-        assertEquals(1.0, calcPriority(edge, model));
-
-        PriorityCalculator priorityCalculator = new PriorityCalculator(model, em);
-        assertEquals(0.5, priorityCalculator.calcPriority(edge, true));
-    }
-
-    private double calcPriority(EdgeIteratorState edge, CustomModel model) {
-        PriorityCalculator priorityCalculator = new PriorityCalculator(model, em);
-        return priorityCalculator.calcPriority(edge, false);
-    }
-}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/weighting/custom/SpeedCalculatorTest.java b/core/src/test/java/com/graphhopper/routing/weighting/custom/SpeedCalculatorTest.java
deleted file mode 100644
index fd93909f8..000000000
--- a/core/src/test/java/com/graphhopper/routing/weighting/custom/SpeedCalculatorTest.java
+++ /dev/null
@@ -1,179 +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.weighting.custom;
-
-import com.graphhopper.routing.ev.EnumEncodedValue;
-import com.graphhopper.routing.ev.RoadClass;
-import com.graphhopper.routing.ev.RoadEnvironment;
-import com.graphhopper.routing.util.CarFlagEncoder;
-import com.graphhopper.util.CustomModel;
-import com.graphhopper.routing.util.EncodingManager;
-import com.graphhopper.routing.util.FlagEncoder;
-import com.graphhopper.storage.GraphBuilder;
-import com.graphhopper.util.EdgeIteratorState;
-import com.graphhopper.util.GHUtility;
-import org.junit.jupiter.api.Test;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-class SpeedCalculatorTest {
-
-    private final EncodingManager em;
-    private final EdgeIteratorState edge;
-
-    SpeedCalculatorTest() {
-        // in this test we use the same edge for every test and check the calculated speed for different custom models
-        // and depending on additional properties of the edge we set in the tests
-        FlagEncoder encoder = new CarFlagEncoder();
-        em = EncodingManager.create(encoder);
-        edge = GHUtility.setSpeed(60, true, true, encoder, new GraphBuilder(em).create().edge(0, 1).setDistance(1000));
-    }
-
-    @Test
-    public void maxSpeedFallback() {
-        // default speed for car is 60km/h
-        assertEquals(60, calcSpeed(edge, new CustomModel()));
-        // use global fallback to limit speed
-        assertEquals(30, calcSpeed(edge, new CustomModel().setMaxSpeedFallback(30.0)));
-    }
-
-    @Test
-    public void maxSpeed() {
-        // here we use max_speed to limit speed
-        CustomModel model = new CustomModel();
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put("*", 25);
-        model.getMaxSpeed().put(RoadClass.KEY, map);
-        assertEquals(25, calcSpeed(edge, model));
-        // ... but setting it to a higher value than the default has no effect
-        map.put("*", 70);
-        assertEquals(60, calcSpeed(edge, model));
-        // also our edge might or might not be affected
-        EnumEncodedValue<RoadClass> roadClass = em.getEnumEncodedValue(RoadClass.KEY, RoadClass.class);
-        edge.set(roadClass, RoadClass.PRIMARY);
-        model.getMaxSpeed().clear();
-        assertEquals(60, calcSpeed(edge, new CustomModel()));
-        map.clear();
-        map.put(RoadClass.PRIMARY.toString(), 10);
-        model.getMaxSpeed().put(RoadClass.KEY, map);
-        assertEquals(10, calcSpeed(edge, model));
-    }
-
-    @Test
-    public void invalidMaxSpeed() {
-        CustomModel model = new CustomModel();
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put("*", 300);
-        model.getMaxSpeed().put(RoadClass.KEY, map);
-        try {
-            assertEquals(60, calcSpeed(edge, model));
-            fail();
-        } catch (IllegalArgumentException e) {
-            assertTrue(e.getMessage().contains("max_speed.road_class cannot be bigger than 140.0, was 300.0"), e.getMessage());
-        }
-        // negative values are not allowed
-        try {
-            map.put("*", -5);
-            assertEquals(60, calcSpeed(edge, model));
-            fail();
-        } catch (IllegalArgumentException e) {
-            assertTrue(e.getMessage().contains("max_speed.road_class cannot be smaller than 0.0, was -5.0"), e.getMessage());
-        }
-    }
-
-    @Test
-    public void maxSpeedMultiple() {
-        EnumEncodedValue<RoadClass> roadClass = em.getEnumEncodedValue(RoadClass.KEY, RoadClass.class);
-        EnumEncodedValue<RoadEnvironment> roadEnvironment = em.getEnumEncodedValue(RoadEnvironment.KEY, RoadEnvironment.class);
-        edge.set(roadClass, RoadClass.PRIMARY);
-        edge.set(roadEnvironment, RoadEnvironment.BRIDGE);
-        Map<String, Object> roadClassMap = new LinkedHashMap<>();
-        roadClassMap.put(RoadClass.PRIMARY.toString(), 40);
-        Map<String, Object> roadEnvironmentMap = new LinkedHashMap<>();
-        roadEnvironmentMap.put(RoadEnvironment.BRIDGE.toString(), 20);
-        CustomModel model = new CustomModel();
-        model.getMaxSpeed().put(RoadClass.KEY, roadClassMap);
-        model.getMaxSpeed().put(RoadEnvironment.KEY, roadEnvironmentMap);
-        // the more restrictive max speed 'wins'
-        assertEquals(20, calcSpeed(edge, model));
-    }
-
-    @Test
-    public void speedFactor() {
-        // here we use speed_factor to adjust speed
-        CustomModel model = new CustomModel();
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put("*", 0.1);
-        model.getSpeedFactor().put(RoadClass.KEY, map);
-        assertEquals(6, calcSpeed(edge, model));
-        // our edge might or might not be affected
-        EnumEncodedValue<RoadClass> roadClass = em.getEnumEncodedValue(RoadClass.KEY, RoadClass.class);
-        edge.set(roadClass, RoadClass.PRIMARY);
-        model.getSpeedFactor().clear();
-        assertEquals(60, calcSpeed(edge, new CustomModel()));
-        map.clear();
-        map.put(RoadClass.PRIMARY.toString(), 0.2);
-        model.getSpeedFactor().put(RoadClass.KEY, map);
-        assertEquals(12, calcSpeed(edge, model));
-    }
-
-    @Test
-    public void invalidSpeedFactor() {
-        CustomModel model = new CustomModel();
-        Map<String, Object> map = new LinkedHashMap<>();
-        map.put("*", 1.1);
-        model.getSpeedFactor().put(RoadClass.KEY, map);
-        try {
-            assertEquals(6, calcSpeed(edge, model));
-            fail();
-        } catch (IllegalArgumentException e) {
-            assertTrue(e.getMessage().contains("speed_factor.road_class cannot be bigger than 1.0, was 1.1"), e.getMessage());
-        }
-    }
-
-    @Test
-    public void speedFactorMultiple() {
-        // use speed_factor to adjust speed
-        EnumEncodedValue<RoadClass> roadClass = em.getEnumEncodedValue(RoadClass.KEY, RoadClass.class);
-        EnumEncodedValue<RoadEnvironment> roadEnvironment = em.getEnumEncodedValue(RoadEnvironment.KEY, RoadEnvironment.class);
-        edge.set(roadClass, RoadClass.PRIMARY);
-        edge.set(roadEnvironment, RoadEnvironment.BRIDGE);
-        Map<String, Object> roadClassMap = new LinkedHashMap<>();
-        roadClassMap.put(RoadClass.PRIMARY.toString(), 0.7);
-        Map<String, Object> roadEnvironmentMap = new LinkedHashMap<>();
-        roadEnvironmentMap.put(RoadEnvironment.BRIDGE.toString(), 0.5);
-        CustomModel model = new CustomModel();
-        model.getSpeedFactor().put(RoadClass.KEY, roadClassMap);
-        model.getSpeedFactor().put(RoadEnvironment.KEY, roadEnvironmentMap);
-        assertEquals(0.35 * 60, calcSpeed(edge, model));
-        // the global max speed fallback can still overwrite our speed factor settings
-        model.setMaxSpeedFallback(5.0);
-        assertEquals(5, calcSpeed(edge, model));
-    }
-
-    private double calcSpeed(EdgeIteratorState edge, CustomModel model) {
-        FlagEncoder encoder = em.fetchEdgeEncoders().iterator().next();
-        SpeedCalculator speedCalculator = new SpeedCalculator(encoder.getMaxSpeed(), model, encoder.getAverageSpeedEnc(), em);
-        return speedCalculator.calcSpeed(edge, false);
-    }
-
-}
\ No newline at end of file
diff --git a/example/src/test/java/com/graphhopper/example/RoutingExampleTest.java b/example/src/test/java/com/graphhopper/example/RoutingExampleTest.java
index e0caf6c5e..7236c770e 100644
--- a/example/src/test/java/com/graphhopper/example/RoutingExampleTest.java
+++ b/example/src/test/java/com/graphhopper/example/RoutingExampleTest.java
@@ -1,12 +1,18 @@
 package com.graphhopper.example;
 
+import com.graphhopper.util.Helper;
 import org.junit.jupiter.api.Test;
 
+import java.io.File;
+
 public class RoutingExampleTest {
 
     @Test
     public void main() {
+        Helper.removeDir(new File("target/routing-graph-cache"));
         RoutingExample.main(new String[]{"../"});
+
+        Helper.removeDir(new File("target/routing-tc-graph-cache"));
         RoutingExampleTC.main(new String[]{"../"});
     }
 }
diff --git a/web/src/test/java/com/graphhopper/http/resources/CustomWeightingRouteResourceLMTest.java b/web/src/test/java/com/graphhopper/http/resources/CustomWeightingRouteResourceLMTest.java
index 9f49d63a9..9c30b98f7 100644
--- a/web/src/test/java/com/graphhopper/http/resources/CustomWeightingRouteResourceLMTest.java
+++ b/web/src/test/java/com/graphhopper/http/resources/CustomWeightingRouteResourceLMTest.java
@@ -23,8 +23,8 @@ import com.graphhopper.config.Profile;
 import com.graphhopper.http.GraphHopperApplication;
 import com.graphhopper.http.GraphHopperServerConfiguration;
 import com.graphhopper.http.util.GraphHopperServerTestConfiguration;
-import com.graphhopper.util.CustomModel;
 import com.graphhopper.routing.weighting.custom.CustomProfile;
+import com.graphhopper.util.CustomModel;
 import com.graphhopper.util.Helper;
 import io.dropwizard.testing.junit5.DropwizardAppExtension;
 import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
@@ -94,9 +94,8 @@ public class CustomWeightingRouteResourceLMTest {
         String yamlQuery = "points: [[1.529106,42.506567], [1.54006,42.511178]]\n" +
                 "profile: car_custom\n" +
                 "priority:\n" +
-                "  road_class:\n" +
-                "    secondary: 1\n" +
-                "    '*': 0.5\n";
+                "  - if: road_class != SECONDARY\n" +
+                "    multiply by: 0.5\n";
         JsonNode yamlNode = queryYaml(yamlQuery, 200).readEntity(JsonNode.class);
         JsonNode path = yamlNode.get("paths").get(0);
         assertEquals(path.get("distance").asDouble(), 1317, 5);
@@ -105,7 +104,12 @@ public class CustomWeightingRouteResourceLMTest {
         yamlQuery = "points: [[1.5274,42.506211], [1.54006,42.511178]]\n" +
                 "profile: car_custom\n" +
                 "priority:\n" +
-                "  road_class: { residential: 0.8, primary: 1, '*': 0.66 }";
+                "  - if: road_class == RESIDENTIAL\n" +
+                "    multiply by: 0.8\n" +
+                "  - else if: road_class == PRIMARY\n" +
+                "    multiply by: 1\n" +
+                "  - else:\n" +
+                "    multiply by: 0.66\n";
         yamlNode = queryYaml(yamlQuery, 200).readEntity(JsonNode.class);
         path = yamlNode.get("paths").get(0);
         assertEquals(path.get("distance").asDouble(), 1707, 5);
@@ -116,8 +120,8 @@ public class CustomWeightingRouteResourceLMTest {
         String yamlQuery = "points: [[1.533365, 42.506211], [1.523924, 42.520605]]\n" +
                 "profile: car_custom\n" +
                 "priority:\n" +
-                "  road_environment:\n" +
-                "    tunnel: 0.1\n";
+                "  - if: road_environment == TUNNEL\n" +
+                "    multiply by: 0.1\n";
         JsonNode yamlNode = queryYaml(yamlQuery, 200).readEntity(JsonNode.class);
         JsonNode path = yamlNode.get("paths").get(0);
         assertEquals(path.get("distance").asDouble(), 2437, 5);
@@ -144,8 +148,8 @@ public class CustomWeightingRouteResourceLMTest {
         String yamlQuery = "points: [[1.540875,42.510672], [1.54212,42.511131]]\n" +
                 "profile: foot_custom\n" +
                 "priority:\n" +
-                "  road_class:\n" +
-                "    steps: 0\n";
+                "  - if: road_class == STEPS\n" +
+                "    multiply by: 0\n";
         JsonNode yamlNode = queryYaml(yamlQuery, 200).readEntity(JsonNode.class);
         JsonNode path = yamlNode.get("paths").get(0);
         assertEquals(path.get("distance").asDouble(), 328, 5);
diff --git a/web/src/test/java/com/graphhopper/http/resources/CustomWeightingRouteResourceTest.java b/web/src/test/java/com/graphhopper/http/resources/CustomWeightingRouteResourceTest.java
index 327376841..6e31b401a 100644
--- a/web/src/test/java/com/graphhopper/http/resources/CustomWeightingRouteResourceTest.java
+++ b/web/src/test/java/com/graphhopper/http/resources/CustomWeightingRouteResourceTest.java
@@ -7,8 +7,8 @@ import com.graphhopper.config.CHProfile;
 import com.graphhopper.http.GraphHopperApplication;
 import com.graphhopper.http.GraphHopperServerConfiguration;
 import com.graphhopper.http.util.GraphHopperServerTestConfiguration;
-import com.graphhopper.util.CustomModel;
 import com.graphhopper.routing.weighting.custom.CustomProfile;
+import com.graphhopper.util.CustomModel;
 import com.graphhopper.util.Helper;
 import io.dropwizard.testing.junit5.DropwizardAppExtension;
 import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
@@ -91,9 +91,11 @@ public class CustomWeightingRouteResourceTest {
 
         // 'blocking' the area either leads to a route that still crosses it (but on a faster road) or to a road
         // going all the way around it depending on the priority, see #2021
-        yamlQuery += "priority:\n" +
+        yamlQuery += "" +
+                "priority:\n" +
                 // a faster road (see #2021)? or maybe do both?
-                "  area_custom1: " + priority + "\n" +
+                "  - if: in_area_custom1\n" +
+                "    multiply by: " + priority + "\n" +
                 "areas:\n" +
                 "  custom1:\n" +
                 "    type: \"Feature\"\n" +
diff --git a/web/src/test/resources/com/graphhopper/http/resources/cargo_bike.yml b/web/src/test/resources/com/graphhopper/http/resources/cargo_bike.yml
index f9485d8a5..40eddcf7b 100644
--- a/web/src/test/resources/com/graphhopper/http/resources/cargo_bike.yml
+++ b/web/src/test/resources/com/graphhopper/http/resources/cargo_bike.yml
@@ -2,27 +2,28 @@
 # profile: bike
 
 # let's assume cargo bikes are e-bikes and we should lower speed to prefer shorter and not faster routes automatically
-max_speed:
-  road_class:
-    primary: 28
-
-# if no other condition matches
-max_speed_fallback: 25
+speed:
+  - if: road_class == PRIMARY
+    limit to: 28
+  - else:
+    limit to: 25
 
 priority:
   # exclude steps
-  road_class:
-    steps: 0
-  surface:
-    sand: 0.5
+  - if: road_class == STEPS
+    multiply by: 0
+  - if: surface == SAND
+    multiply by: 0.5
   # prefer better tracks
-  track_type: { grade1: 1.0, "*": 0.9 }
+  - if: track_type != MISSING && track_type != GRADE1
+    multiply by: 0.9
   # prefer official bike routes
-  bike_network: { other: 0.5 }
+  - if: bike_network == OTHER
+    multiply by: 0.5
   # avoid all situations where we have to get off the bike
-  get_off_bike:
-    "true": 0.5
-  max_height:
-    "<2.3": 0
-  max_width:
-    "<1.2": 0
+  - if: get_off_bike
+    multiply by: 0.5
+  - if: max_height < 2.3
+    multiply by: 0
+  # oneliner
+  - { if: "max_width < 1.2", multiply by: 0 }
diff --git a/web/src/test/resources/com/graphhopper/http/resources/json_bike.json b/web/src/test/resources/com/graphhopper/http/resources/json_bike.json
index a9e4bc4b6..ebdcd7e40 100644
--- a/web/src/test/resources/com/graphhopper/http/resources/json_bike.json
+++ b/web/src/test/resources/com/graphhopper/http/resources/json_bike.json
@@ -1,12 +1,8 @@
 {
-  "max_speed": {
-    "road_class": {
-      "primary": 28
-    }
-  },
-  "priority": {
-    "max_width": {
-      "<1.2": 0
-    }
-  }
+  "speed": [
+    { "if": "road_class==PRIMARY", "limit to": 28 }
+  ],
+  "priority": [
+    { "if": "max_width < 1.2", "multiply by": 0 }
+  ]
 }
\ No newline at end of file
diff --git a/web/src/test/resources/com/graphhopper/http/resources/truck.yml b/web/src/test/resources/com/graphhopper/http/resources/truck.yml
index c96744c6c..dcaa9518f 100644
--- a/web/src/test/resources/com/graphhopper/http/resources/truck.yml
+++ b/web/src/test/resources/com/graphhopper/http/resources/truck.yml
@@ -1,30 +1,6 @@
 # For a custom request you need to specify a profile (with weighting=custom) but on import we don't need this
 # profile: car
 
-# The formula is defined in CustomWeighting, where the speed_factor can be used to decrease speed.
-# The minimum value is 0 and the maximum is 1. If two conditions are satisfied the values are
-# multiplied e.g. if road_class==motorway and road_environment==tunnel, then the resulting speed is
-# average_speed*0.85*0.9.
-speed_factor:
-  road_environment:
-    tunnel: 0.85
-  # trucks should be a bit slower on certain road classes compared to the 'base' car
-  # a more compact JSON-way to list the properties, fully YAML-compatible:
-  road_class: { motorway: 0.85, primary: 0.9 }
-
-# You can lower the average speed for certain conditions via this max_speed entry. The speed_factor is applied before
-# this operation. See the following example that sets the maximum speed to 95km/h for motorways.
-max_speed:
-  road_class:
-    motorway: 95
-    residential: 30
-  road_environment:
-    bridge: 85
-
-# Trucks are slower so limit all speed to this value. In km/h
-# If none of the conditions in the map above apply use the fallback, if specified:
-max_speed_fallback: 100
-
 # This term changes the influence of the distance. I.e. longer roads get a higher cost.
 # The distance_influence is independent of the edge properties and does not influence the ETA. The default is 70 (seconds/1km).
 # Let's assume a route that takes 1000sec and is 10km long, then a value of 30 means that I would like to drive maximum 11km
@@ -33,32 +9,62 @@ distance_influence: 90
 
 # Now we want to set a preference without changing the taken time. The default priority is 1 and does not change something.
 # The minimum value is 0 (block access). A value lower than 1 will avoid it. Values higher than 1 are not allowed.
-# To prefer a certain case you lower the priority of all other entries via the catch-all key "*",
-# see the road_class entry where motorways are preferred. If two conditions are met the values will be multiplied.
+# To prefer a certain case you lower the priority of all other entries. See the road_class entry where motorways are preferred.
+# If two conditions are met the values will be multiplied except when first_match is used.
 priority:
-  road_class:
-    motorway: 1.0
-    residential: 0.4
-    "*": 0.9
-    # and if you do not want that tracks go over tracks just exclude them via
-    # track: 0
-  # let's assume we transport gas: so NEVER go on restricted roads with hazmat==no
-  hazmat: { no: 0 }
+  - if: road_class == RESIDENTIAL
+    multiply by: 0.4
+  - else if: road_class != MOTORWAY
+    multiply by: 0.9
+    # and if you do not want that routes go over tracks just exclude them:
+    # else if: road_class==TRACK
+    # multiply by: 0
+  # let's assume we transport gas: so NEVER go on restricted roads with hazmat==NO
+  - if: hazmat == NO
+    multiply by: 0
   # avoid destination-only roads
   # TODO include private and delivery only access for base car profile so that we can avoid it here, but not exclude it
-  road_access: { destination: 0.1 }
+  - if: road_access == DESTINATION
+    multiply by: 0.1
   # avoid turns if possible and links are one simple indication for that
-  road_class_link:
-    true: 0.5
+  - if: road_class_link
+    multiply by: 0.5
   # avoid toll roads
-  toll: { no: 1, "*": 0.5 }
+  - if: toll != NO
+    multiply by: 0.5
   # avoid a certain area
-  area_custom1: 0.5
-  max_weight: { "<4.5": 0 }
-  max_height: { "<3.8": 0 }
-  max_width: { "<2.5": 0 }
+  - if: in_area_custom1
+    multiply by: 0.5
+  - if: max_weight < 4.5
+    multiply by: 0
+  - if: max_height < 3.8
+    multiply by: 0
+  - if: max_width < 2.5
+    multiply by: 0
+
+# The formula is defined in CustomWeighting, where the "multiply by" operation can be used to decrease speed.
+# The minimum value is 0 and the maximum is 1. If two conditions are satisfied the values are
+# multiplied e.g. if road_class==MOTORWAY and road_environment==TUNNEL, then the resulting speed is
+# average_speed*0.85*0.9
+speed:
+  - if: road_environment == TUNNEL
+    multiply by: 0.85
+  # trucks should be a bit slower on certain road classes compared to the 'base' car
+  - if: road_class == MOTORWAY
+    multiply by: 0.85
+  - if: road_class == PRIMARY
+    multiply by: 0.9
+  # You can lower the average speed for certain conditions via the "limit to" operation:
+  - if: road_class == MOTORWAY
+    limit to: 95
+  - if: road_class == RESIDENTIAL
+    limit to: 30
+  - if: road_environment == BRIDGE
+    limit to: 85
+  - else:
+    limit to: 100
 
 areas:
   custom1:
     type: "Feature"
-    geometry: { type: "Polygon", coordinates: [[[13.722, 51.053], [13.722, 51.055], [13.731, 51.055], [13.731, 51.053], [13.722, 51.053]]] }
\ No newline at end of file
+    geometry: { type: "Polygon", coordinates: [ [ [ 13.722, 51.053 ], [ 13.722, 51.055 ], [ 13.731, 51.055 ], [ 13.731, 51.053 ], [ 13.722, 51.053 ] ] ] }
\ No newline at end of file