Repository: graphhopper/graphhopper
Pull request #1829 (2019-12-18 14:18:40)

Use JTS to determine applicable spatial rules
Builds on top of #1827 and uses JTS classes to handle border geometries and SpatialLookup.

This should also solve #1680 as JTS polygons are able to deal with holes.

karussell: Cool, thanks! Probably after this we should get rid of our own Polygon class and also use the JTS version elsewhere.
otbutz: > Cool, thanks! Probably after this we should get rid of our own Polygon class and also use the JTS version elsewhere.

`BBox` would also be a good candidate for removal.
boldtrn: Really nice, thanks for the great work. Did you have a chance to compare the performance of the JTS lookup vs. the array lookup?
otbutz: > Really nice, thanks for the great work. Did you have a chance to compare the performance of the JTS lookup vs. the array lookup?

Just did a small benchmark with a dummy rule for each country in our geojson file on my machine with an i7-4790 and OpenJDK 11. Looking up the rules for 1 million random points in central europe takes about 1.5 seconds.

If i change the underlying index from STRtree to QuadTree the execution time goes up to about 2 seconds.
![Screenshot_2020-01-03 Bounding Box Tool Metadata Enrichment for Catalogue Records by Visually Selecting Geographic Coordina](https://user-images.githubusercontent.com/22315436/71720702-dd9c5f80-2e22-11ea-9d0c-c3eaf1394510.png)


```java
SpatialRuleLookup spatialRuleLookup = SpatialRuleLookupBuilder.buildIndex(Collections.singletonList(Jackson.newObjectMapper().readValue(reader, JsonFeatureCollection.class)), "ISO_A3", rulePerCountryFactory);

List<GHPoint> randomPoints = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
    double lat = 46d + Math.random() * 7d;
    double lon = 6d + Math.random() * 21d;
    randomPoints.add(new GHPoint(lat, lon));
}

long start = System.nanoTime();
for (GHPoint point : randomPoints) {
    spatialRuleLookup.lookupRule(point);
}
long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
System.out.println("Took: " + duration + "ms");
```

I ran the same test against the array lookup but had to limit the number of countries in order for them to fit into the index. With the first 100 countries the same benchmark takes about 0.5 seconds but i noticed that building the index is rather slow with 50 seconds. The JTS approach builds the complete world index with 255 countries in about 30ms.

I than reran the benchmark using the same 100 countries and the JTS index finished in around 0.9 seconds with an index build time of around 20ms.

Considering the startup costs the array lookup would need to process about 80 million points for the startup delay to amortize.
otbutz: > SpatialRuleLookupBuilderTest.testSpeed:246 » OutOfMemory Java heap space

What is the limit for the test builds?
karussell: We limit it explicitly to 100MB to detect memory issues very early: https://github.com/graphhopper/graphhopper/blob/master/pom.xml#L118. How much more would be required?
otbutz: @karussell i've lowered the number of points for the test from 5 to 1 million.
boldtrn: > With the first 100 countries the same benchmark takes about 0.5 seconds but i noticed that building the index is rather slow with 50 seconds.

Yes, that's somewhat expected. The lookup is currently optimized for look up performance. For me building the index was not that time critical. This is only done during the import, where time (adding a few seconds to minutes) plays only a minor role, but we might need to lookup > 200.000.000 edges for world wide imports. Also if we ever want to do the lookup during the route calculation.

Thanks for providing some benchmark data and this nice PR :+1: 
otbutz: > but we might need to lookup > 200.000.000 edges for world wide imports. 

The JTS approach should be fast enough to be used during the import. The benchmark is a bit pessimistic as i measured it with one rule for every country on the planet which results in an index with ~2.6k entries.

> Also if we ever want to do the lookup during the route calculation.

There is still room for improvement. The hit-rate of the spatial index seems to be a good candidate for optimization. Just take a look at the bounding box of Austria which covers half of Bavaria:

![Screenshot_2020-01-08 Bounding Box Tool Metadata Enrichment for Catalogue Records by Visually Selecting Geographic Coordina](https://user-images.githubusercontent.com/22315436/71983029-2e96c400-3226-11ea-9126-1eadb7024a65.png)

Each point in the southern region of Bavaria needs to be tested against both polygons as the fast index/envelope shortcut doesn't work here.

We could split bigger polygons in order to keep their Envelope small and therefor closer to the actual borders.

otbutz: Did a bit of profiling using VisualVM while recording roughly 2 minutes of execution time:

## Whole world index (2.6k polygons)
![Screenshot_20200108_172850](https://user-images.githubusercontent.com/22315436/71996384-6c064c00-323c-11ea-93ab-d0881c524e89.png)

## Central europe index (54 polygons)
![Screenshot_20200108_182351](https://user-images.githubusercontent.com/22315436/72000841-74fb1b80-3244-11ea-98cd-e9cd372d2b1e.png)

## Benchmark results

|   | Whole world  | Europe |
| ------------- | ------------- | ------------- |
| 1 million points  | 1.5s  | 1s  |
| 5 million points  | 6.2s  | 4.5s  |

Seems like the size of the index isn't that crucial which means that we would need to lower the amount unnecessary PIP checks in order to speed things up. Note that the number of invocations of `SpatialRuleContainer.containsProperly(Point)` is roughly twice the amount of rule lookups.
otbutz: With the last change the lookup of 5 million points is down to 3.5s
otbutz: @karussell anything missing?
karussell: As this is based on #1827 should we close #1827 when merging here? Or should we merge #1827 first?
otbutz: We can close #1827 when we merge this branch. I just thought it would be better to break my changes into smaller units.
boldtrn: Since we change the config value to: `spatial_rules.borders_directory`, maybe we should add this to the changelog.txt? Otherwise people might wonder if SpatialRules are suddenly not loaded anymore?
otbutz: > Since we change the config value to: `spatial_rules.borders_directory`, maybe we should add this to the changelog.txt? Otherwise people might wonder if SpatialRules are suddenly not loaded anymore?

Thanks! Completely forgot to cherry-pick this commit: https://github.com/graphhopper/graphhopper/pull/1827/commits/9708e45edf1fc6779744ace5cc006e38dcac163d
otbutz: @boldtrn i've addressed the issues you mentioned and rebased on the current master
boldtrn: > i've addressed the issues you mentioned

Thanks a lot. I added a lost minute note, sorry :).

Patch:
diff --git a/config-example.yml b/config-example.yml
index 66a2b8062..04b7f2805 100644
--- a/config-example.yml
+++ b/config-example.yml
@@ -132,8 +132,8 @@ graphhopper:
 
 
   # Spatial Rules require you to provide Polygons in which the rules are enforced
-  # The line below contains the default location for these rules
-  # spatial_rules.location: core/files/spatialrules/countries.geo.json
+  # The line below contains the default location for the files which define these borders
+  # spatial_rules.borders_directory: core/files/spatialrules
 
   # You can define the maximum BBox for which spatial rules are loaded.
   # You might want to do this if you are only importing a small area and don't need rules for other countries.
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/AbstractSpatialRule.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/AbstractSpatialRule.java
index 38ca9ce79..4f5a56572 100644
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/AbstractSpatialRule.java
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/AbstractSpatialRule.java
@@ -17,18 +17,18 @@
  */
 package com.graphhopper.routing.util.spatialrules;
 
-import com.graphhopper.util.shapes.Polygon;
-
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import org.locationtech.jts.geom.Polygon;
+
 /**
  * @author Robin Boldt
  */
 public abstract class AbstractSpatialRule implements SpatialRule {
 
-    protected List<Polygon> polygons = Collections.EMPTY_LIST;
+    protected List<Polygon> polygons = Collections.emptyList();
 
     public List<Polygon> getBorders() {
         return polygons;
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/CountriesSpatialRuleFactory.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/CountriesSpatialRuleFactory.java
index 0125eafaa..34e802c56 100644
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/CountriesSpatialRuleFactory.java
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/CountriesSpatialRuleFactory.java
@@ -2,10 +2,11 @@ package com.graphhopper.routing.util.spatialrules;
 
 import com.graphhopper.routing.util.spatialrules.countries.AustriaSpatialRule;
 import com.graphhopper.routing.util.spatialrules.countries.GermanySpatialRule;
-import com.graphhopper.util.shapes.Polygon;
 
 import java.util.List;
 
+import org.locationtech.jts.geom.Polygon;
+
 public class CountriesSpatialRuleFactory implements SpatialRuleLookupBuilder.SpatialRuleFactory {
     @Override
     public SpatialRule createSpatialRule(String id, List<Polygon> polygons) {
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRule.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRule.java
index f3e8d72f1..95e9172d3 100644
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRule.java
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRule.java
@@ -18,9 +18,11 @@
 package com.graphhopper.routing.util.spatialrules;
 
 import com.graphhopper.routing.profiles.RoadAccess;
-import com.graphhopper.util.shapes.Polygon;
 
 import java.util.List;
+import java.util.Objects;
+
+import org.locationtech.jts.geom.Polygon;
 
 /**
  * Defines rules that are valid for a certain region, e.g. a country.
@@ -87,5 +89,18 @@ public interface SpatialRule {
         public String toString() {
             return "SpatialRule.EMPTY";
         }
+        
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof SpatialRule)) {
+                return false;
+            }
+            return Objects.equals(getId(), ((SpatialRule) obj).getId());
+        }
+        
+        @Override
+        public int hashCode() {
+            return getId().hashCode();
+        }
     };
 }
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleContainer.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleContainer.java
index 45a39d091..df4ad5362 100644
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleContainer.java
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleContainer.java
@@ -19,29 +19,47 @@ package com.graphhopper.routing.util.spatialrules;
 
 import java.util.*;
 
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+import org.locationtech.jts.geom.prep.PreparedGeometry;
+import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
+
 /**
- * This class contains a collection of SpatialRule and is used for the implementation SpatialRuleLookupArray.
+ * This class contains a collection of SpatialRule which are valid for a certain Polygon.
  *
  * @author Robin Boldt
+ * @author Thomas Butz
  */
 class SpatialRuleContainer {
+    private static final PreparedGeometryFactory PREP_GEOM_FACTORY = new PreparedGeometryFactory();
+    private static final GeometryFactory FAC = new GeometryFactory();
+    private static final int GRID_SIZE = 10;
+    private static final double COORD_EPSILON = 0.00001;
 
-    final Set<SpatialRule> rules = new LinkedHashSet<>();
-
-    public SpatialRuleContainer addRule(SpatialRule spatialRule) {
-        rules.add(spatialRule);
-        return this;
+    private final PreparedGeometry preparedPolygon;
+    private final Set<SpatialRule> rules = new LinkedHashSet<>();
+    private final List<Envelope> filledLines;
+    
+    public SpatialRuleContainer(Polygon polygon) {
+        this(PREP_GEOM_FACTORY.create(polygon));
+    }
+    
+    private SpatialRuleContainer(PreparedGeometry preparedPolygon) {
+        this.preparedPolygon = preparedPolygon;
+        this.filledLines = findFilledLines(preparedPolygon);
     }
 
-    public SpatialRuleContainer addRules(Collection<SpatialRule> rules) {
-        this.rules.addAll(rules);
-        return this;
+    public void addRule(SpatialRule spatialRule) {
+        rules.add(spatialRule);
     }
 
     /**
      * Returns a list of all spatial rules including the EMPTY one.
      */
-    Collection<SpatialRule> getRules() {
+    public Collection<SpatialRule> getRules() {
         return rules;
     }
 
@@ -49,27 +67,51 @@ class SpatialRuleContainer {
         return this.rules.size();
     }
 
-    SpatialRule first() {
-        return this.rules.iterator().next();
-    }
-
-    @Override
-    public int hashCode() {
-        return rules.hashCode();
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (o instanceof SpatialRuleContainer) {
-            if (this.rules.equals(((SpatialRuleContainer) o).getRules()))
+    public boolean containsProperly(Point point) {
+        Coordinate coord = point.getCoordinate();
+        
+        for (Envelope line : filledLines) {
+            if (line.covers(coord)) {
                 return true;
+            }
         }
-        return false;
+        
+        return preparedPolygon.containsProperly(point);
     }
 
     public SpatialRuleContainer copy() {
-        SpatialRuleContainer container = new SpatialRuleContainer();
-        container.addRules(this.rules);
+        SpatialRuleContainer container = new SpatialRuleContainer(this.preparedPolygon);
+        container.rules.addAll(this.rules);
         return container;
     }
+    
+    private static List<Envelope> findFilledLines(PreparedGeometry prepGeom) {
+        List<Envelope> lines = new ArrayList<>();
+        
+        Envelope bbox = prepGeom.getGeometry().getEnvelopeInternal();
+        double tileWidth  = bbox.getWidth()  / GRID_SIZE;
+        double tileHeight = bbox.getHeight() / GRID_SIZE;
+
+        Envelope tile = new Envelope();
+        Envelope line;
+        for (int row = 0; row < GRID_SIZE; row++) {
+            line = null;
+            for (int column = 0; column < GRID_SIZE; column++) {
+                double minX = bbox.getMinX() + (column * tileWidth);
+                double minY = bbox.getMinY() + (row * tileHeight);
+                tile.init(minX, minX + tileWidth, minY, minY + tileHeight);
+                
+                if (prepGeom.covers(FAC.toGeometry(tile))) {
+                    if (line != null && Math.abs(line.getMaxX() - tile.getMinX()) < COORD_EPSILON) {
+                        line.expandToInclude(tile);
+                    } else {
+                        line = new Envelope(tile);
+                        lines.add(line);
+                    }
+                }
+            }
+        }
+        
+        return lines;
+    }
 }
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookup.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookup.java
index c5b9ff85f..213c26d9d 100644
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookup.java
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookup.java
@@ -17,7 +17,8 @@
  */
 package com.graphhopper.routing.util.spatialrules;
 
-import com.graphhopper.util.shapes.BBox;
+import org.locationtech.jts.geom.Envelope;
+
 import com.graphhopper.util.shapes.GHPoint;
 
 /**
@@ -63,7 +64,7 @@ public interface SpatialRuleLookup {
     /**
      * @return the bounds of the SpatialRuleLookup
      */
-    BBox getBounds();
+    Envelope getBounds();
 
     SpatialRuleLookup EMPTY = new SpatialRuleLookup() {
         @Override
@@ -92,8 +93,8 @@ public interface SpatialRuleLookup {
         }
 
         @Override
-        public BBox getBounds() {
-            return new BBox(-180, 180, -90, 90);
+        public Envelope getBounds() {
+            return new Envelope(-180d, 180d, -90d, 90d);
         }
     };
 }
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupArray.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupArray.java
deleted file mode 100644
index a6d576ff0..000000000
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupArray.java
+++ /dev/null
@@ -1,276 +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.spatialrules;
-
-import com.graphhopper.util.shapes.BBox;
-import com.graphhopper.util.shapes.GHPoint;
-import com.graphhopper.util.shapes.Polygon;
-
-import java.util.*;
-
-/**
- * SpatialRuleLookup implementation using an array as data structure. Currently limited to 255 ruleContainers
- * The covered area is indexed as tiles, with every tile being "quadratic" having the same degree length on every side.
- *
- * @author Robin Boldt
- */
-public class SpatialRuleLookupArray implements SpatialRuleLookup {
-
-    // resolution in full decimal degrees
-    private final double resolution;
-    // When filling a tile with a rule we do a 5 point check, diff is the distance from the center we place a check point
-    private final double checkDiff;
-    private final BBox bounds;
-    private final boolean exact;
-    private final int EMPTY_RULE_INDEX = 0;
-
-    private final byte[][] lookupArray;
-    private final List<SpatialRuleContainer> ruleContainers = new ArrayList<>();
-    private final Map<SpatialRule, Integer> singleRulesIndices = new HashMap<>();
-    private final List<SpatialRule> singleRules = new ArrayList<>();
-
-    /**
-     * @param spatialRules the spatial rules
-     * @param resolution   of the array in decimal degrees, see: https://en.wikipedia.org/wiki/Decimal_degrees
-     *                     The downside of using decimal degrees is that this is not fixed to a certain m range as
-     * @param exact        if exact it will also perform a polygon contains for border tiles, might fail for small holes
-     *                     in the Polygon that are not represented in the tile array.
-     * @param bounds       create the SpatialRuleLookup for the given BBox
-     */
-    public SpatialRuleLookupArray(List<SpatialRule> spatialRules, double resolution, boolean exact, BBox bounds) {
-        if (!bounds.isValid())
-            throw new IllegalStateException("Bounds are not valid: " + bounds);
-
-        this.bounds = bounds;
-        if (resolution < 1e-100)
-            throw new IllegalArgumentException("resolution cannot be that high " + resolution);
-
-        this.resolution = resolution;
-        this.checkDiff = (resolution / 2) - (resolution / 10);
-        this.exact = exact;
-
-        lookupArray = new byte[getNumberOfXGrids()][getNumberOfYGrids()];
-        addSingleRule(SpatialRule.EMPTY);
-        ruleContainers.add(new SpatialRuleContainer() {
-            {
-                this.rules.add(SpatialRule.EMPTY);
-            }
-
-            @Override
-            public SpatialRuleContainer addRule(SpatialRule spatialRule) {
-                throw new IllegalArgumentException("Cannot add to empty rule container");
-            }
-
-            @Override
-            public SpatialRuleContainer addRules(Collection<SpatialRule> rules) {
-                throw new IllegalArgumentException("Cannot add to empty rule container");
-            }
-        });
-
-        for (SpatialRule spatialRule : spatialRules) {
-            addRuleInternal(spatialRule);
-        }
-    }
-
-    private int getNumberOfYGrids() {
-        return (int) Math.ceil(Math.abs(bounds.maxLat - bounds.minLat) / resolution);
-    }
-
-    private int getNumberOfXGrids() {
-        return (int) Math.ceil(Math.abs(bounds.maxLon - bounds.minLon) / resolution);
-    }
-
-    @Override
-    public SpatialRule lookupRule(double lat, double lon) {
-        if (lon < bounds.minLon || lon > bounds.maxLon || lat < bounds.minLat || lat > bounds.maxLat)
-            return SpatialRule.EMPTY;
-
-        int xIndex = getXIndexForLon(lon);
-        int yIndex = getYIndexForLat(lat);
-        int ruleIndex = getRuleContainerIndex(xIndex, yIndex);
-        SpatialRuleContainer ruleContainer = ruleContainers.get(ruleIndex);
-        if (ruleContainer.size() == 1) {
-            if (!exact || !isBorderTile(xIndex, yIndex, ruleIndex))
-                return ruleContainer.first();
-        }
-
-        for (SpatialRule rule : ruleContainer.getRules()) {
-            if (rule.equals(SpatialRule.EMPTY))
-                continue;
-
-            for (Polygon p : rule.getBorders()) {
-                if (p.contains(lat, lon)) {
-                    return rule;
-                }
-            }
-        }
-
-        return SpatialRule.EMPTY;
-    }
-
-    private int getRuleContainerIndex(int xIndex, int yIndex) {
-        if (xIndex < 0 || xIndex >= lookupArray.length) {
-            return EMPTY_RULE_INDEX;
-        }
-        if (yIndex < 0 || yIndex >= lookupArray[0].length) {
-            return EMPTY_RULE_INDEX;
-        }
-        return castByteToInt(lookupArray[xIndex][yIndex]);
-    }
-
-    /**
-     * Might fail for small holes that do not occur in the array
-     */
-    private boolean isBorderTile(int xIndex, int yIndex, int ruleIndex) {
-        for (int i = -1; i < 2; i++) {
-            for (int j = -1; j < 2; j++) {
-                if (i != xIndex && j != yIndex)
-                    if (ruleIndex != getRuleContainerIndex(i, j))
-                        return true;
-            }
-        }
-        return false;
-    }
-
-    @Override
-    public SpatialRule lookupRule(GHPoint point) {
-        return lookupRule(point.getLat(), point.getLon());
-    }
-
-    private int getXIndexForLon(double lon) {
-        if (lon < bounds.minLon)
-            return 0;
-        return (int) Math.floor(Math.abs(lon - bounds.minLon) / resolution);
-    }
-
-    private int getYIndexForLat(double lat) {
-        if (lat < bounds.minLat)
-            return 0;
-        return (int) Math.floor(Math.abs(lat - bounds.minLat) / resolution);
-    }
-
-    private void addRuleInternal(SpatialRule rule) {
-        if (rule == null)
-            throw new IllegalArgumentException("rule cannot be null");
-
-        if (rule.equals(SpatialRule.EMPTY))
-            throw new IllegalArgumentException("rule cannot be EMPTY");
-
-        addSingleRule(rule);
-        int ruleContainerIndex = addRuleContainer(new SpatialRuleContainer().addRule(rule));
-        for (Polygon polygon : rule.getBorders()) {
-            for (int xIdx = getXIndexForLon(polygon.getMinLon()); xIdx < getXIndexForLon(polygon.getMaxLon()) + 1; xIdx++) {
-                for (int yIdx = getYIndexForLat(polygon.getMinLat()); yIdx < getYIndexForLat(polygon.getMaxLat()) + 1; yIdx++) {
-                    if (xIdx >= lookupArray.length || yIdx >= lookupArray[0].length) {
-                        continue;
-                    }
-
-                    GHPoint center = getCoordinatesForIndex(xIdx, yIdx);
-                    // TODO: Consider creating a new method in Polygon that does the 5 checks - p.partOfTile?
-                    if (polygon.contains(center.lat, center.lon) ||
-                            polygon.contains(center.getLat() - checkDiff, center.getLon() - checkDiff) ||
-                            polygon.contains(center.getLat() - checkDiff, center.getLon() + checkDiff) ||
-                            polygon.contains(center.getLat() + checkDiff, center.getLon() - checkDiff) ||
-                            polygon.contains(center.getLat() + checkDiff, center.getLon() + checkDiff)) {
-
-                        if (lookupArray[xIdx][yIdx] == EMPTY_RULE_INDEX) {
-                            lookupArray[xIdx][yIdx] = (byte) ruleContainerIndex;
-                        } else {
-                            // Merge Rules
-                            SpatialRuleContainer curContainer = getContainerFor2DIndex(xIdx, yIdx);
-                            SpatialRuleContainer newContainer = curContainer.copy().addRule(rule);
-                            int newRuleContainerIndex = addRuleContainer(newContainer);
-                            lookupArray[xIdx][yIdx] = (byte) newRuleContainerIndex;
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    private void addSingleRule(SpatialRule rule) {
-        int index = singleRules.indexOf(rule);
-        if (index >= 0)
-            throw new IllegalArgumentException("Rule " + rule + " already contained at " + index + ". " + ((index >= ruleContainers.size() ? "" : "Existing:" + ruleContainers.get(index))));
-
-        singleRulesIndices.put(rule, singleRules.size());
-        singleRules.add(rule);
-    }
-
-    public SpatialRule getSpatialRule(int id) {
-        if (id < 0 || id >= ruleContainers.size())
-            throw new IllegalArgumentException("SpatialRuleId " + id + " is illegal");
-
-        SpatialRule rule = singleRules.get(id);
-        if (rule == null)
-            throw new IllegalArgumentException("SpatialRuleId " + id + " not found");
-        return rule;
-    }
-
-    /**
-     * This method adds the container if no such rule container exists in this lookup and returns the index otherwise.
-     */
-    private int addRuleContainer(SpatialRuleContainer container) {
-        int newIndex = this.ruleContainers.indexOf(container);
-        if (newIndex >= 0)
-            return newIndex;
-
-        newIndex = ruleContainers.size();
-        if (newIndex >= 255)
-            throw new IllegalStateException("No more spatial rule container fit into this lookup as 255 combination of ruleContainers reached");
-
-        this.ruleContainers.add(container);
-        return newIndex;
-    }
-
-    @Override
-    public int getSpatialId(SpatialRule rule) {
-        if (rule == null)
-            throw new IllegalArgumentException("rule parameter cannot be null");
-
-        Integer integ = singleRulesIndices.get(rule);
-        if (integ == null)
-            throw new IllegalArgumentException("Cannot find rule " + rule);
-        return integ;
-    }
-
-    private int castByteToInt(byte b) {
-        return b & 0xFF;
-    }
-
-    private SpatialRuleContainer getContainerFor2DIndex(int x, int y) {
-        return this.ruleContainers.get(getRuleContainerIndex(x, y));
-    }
-
-    private GHPoint getCoordinatesForIndex(int x, int y) {
-        double lon = bounds.minLon + x * resolution + resolution / 2;
-        double lat = bounds.minLat + y * resolution + resolution / 2;
-        return new GHPoint(lat, lon);
-    }
-
-    @Override
-    public int size() {
-        return singleRules.size();
-    }
-
-    @Override
-    public BBox getBounds() {
-        return bounds;
-    }
-
-}
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupBuilder.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupBuilder.java
index 07dcbb99a..165811c0a 100644
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupBuilder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupBuilder.java
@@ -2,14 +2,17 @@ package com.graphhopper.routing.util.spatialrules;
 
 import com.graphhopper.json.geo.JsonFeature;
 import com.graphhopper.json.geo.JsonFeatureCollection;
-import com.graphhopper.util.shapes.BBox;
-import com.graphhopper.util.shapes.Polygon;
+
+import org.locationtech.jts.geom.Envelope;
 import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.Polygon;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 
 import static com.graphhopper.util.Helper.toLowerCase;
@@ -17,10 +20,17 @@ import static com.graphhopper.util.Helper.toLowerCase;
 public class SpatialRuleLookupBuilder {
 
     public interface SpatialRuleFactory {
-        SpatialRule createSpatialRule(String id, final List<Polygon> polygons);
+        SpatialRule createSpatialRule(String id, final List<Polygon> borders);
     }
 
     private static final Logger logger = LoggerFactory.getLogger(SpatialRuleLookupBuilder.class);
+    
+    private static final Comparator<SpatialRule> SPATIAL_ID_COMP = new Comparator<SpatialRule>() {
+        @Override
+        public int compare(SpatialRule o1, SpatialRule o2) {
+            return o1.getId().compareTo(o2.getId());
+        }
+    };
 
     /**
      * Builds a SpatialRuleLookup by passing the provided JSON features into the provided
@@ -29,63 +39,63 @@ public class SpatialRuleLookupBuilder {
      * <p>
      * See {@link SpatialRuleLookup} and {@link SpatialRule}.
      *
-     * @param jsonFeatureCollection a feature collection
-     * @param jsonIdField           the name of a property in that feature collection which serves as an id
-     * @param spatialRuleFactory    a factory which is called with all the (id, geometry) pairs.
-     *                              It should provide a SpatialRule for each id it knows about,
-     *                              and SpatialRule.EMPTY otherwise.
-     * @param maxBBox               limit the maximum BBox of the SpatialRuleLookup to the given BBox
+     * @param jsonFeatureCollections a List of feature collections
+     * @param jsonIdField            the name of a property in that feature collection which serves as an id
+     * @param spatialRuleFactory     a factory which is called with all the (id, geometry) pairs.
+     *                               It should provide a SpatialRule for each id it knows about,
+     *                               and SpatialRule.EMPTY otherwise.
+     * @param maxBBox                limit the maximum BBox of the SpatialRuleLookup to the given Envelope
      * @return the fully constructed SpatialRuleLookup.
      */
-    public static SpatialRuleLookup buildIndex(JsonFeatureCollection jsonFeatureCollection, String jsonIdField,
-                                               SpatialRuleFactory spatialRuleFactory, double resolution, BBox maxBBox) {
-        BBox polygonBounds = BBox.createInverse(false);
+    public static SpatialRuleLookup buildIndex(List<JsonFeatureCollection> jsonFeatureCollections, String jsonIdField,
+                                               SpatialRuleFactory spatialRuleFactory, Envelope maxBBox) {
+        Envelope envelope = new Envelope();
         List<SpatialRule> spatialRules = new ArrayList<>();
 
-        for (int jsonFeatureIdx = 0; jsonFeatureIdx < jsonFeatureCollection.getFeatures().size(); jsonFeatureIdx++) {
-            JsonFeature jsonFeature = jsonFeatureCollection.getFeatures().get(jsonFeatureIdx);
-            String id = jsonIdField.isEmpty() || toLowerCase(jsonIdField).equals("id") ? jsonFeature.getId() : (String) jsonFeature.getProperty(jsonIdField);
-            if (id == null || id.isEmpty())
-                throw new IllegalArgumentException("ID cannot be empty but was for JsonFeature " + jsonFeatureIdx);
-
-            List<Polygon> borders = new ArrayList<>();
-            for (int i = 0; i < jsonFeature.getGeometry().getNumGeometries(); i++) {
-                Geometry poly = jsonFeature.getGeometry().getGeometryN(i);
-                if (poly instanceof org.locationtech.jts.geom.Polygon)
-                    borders.add(Polygon.create((org.locationtech.jts.geom.Polygon) poly));
-                else
-                    throw new IllegalArgumentException("Geometry for " + id + " (" + i + ") not supported " + poly.getClass().getSimpleName());
-            }
-            SpatialRule spatialRule = spatialRuleFactory.createSpatialRule(id, borders);
-            if (spatialRule != SpatialRule.EMPTY) {
-                spatialRules.add(spatialRule);
-                for (Polygon polygon : spatialRule.getBorders()) {
-                    polygonBounds.update(polygon.getMinLat(), polygon.getMinLon());
-                    polygonBounds.update(polygon.getMaxLat(), polygon.getMaxLon());
+        for (JsonFeatureCollection featureCollection : jsonFeatureCollections) {
+            for (JsonFeature jsonFeature : featureCollection.getFeatures()) {
+                String id = jsonIdField.isEmpty() || toLowerCase(jsonIdField).equals("id") ? jsonFeature.getId() : (String) jsonFeature.getProperty(jsonIdField);
+                if (id == null || id.isEmpty())
+                    throw new IllegalArgumentException("ID cannot be empty but was for JsonFeature " + featureCollection.getFeatures().indexOf(jsonFeature));
+
+                List<Polygon> borders = new ArrayList<>();
+                for (int i = 0; i < jsonFeature.getGeometry().getNumGeometries(); i++) {
+                    Geometry poly = jsonFeature.getGeometry().getGeometryN(i);
+                    if (poly instanceof Polygon)
+                        borders.add((Polygon) poly);
+                    else
+                        throw new IllegalArgumentException("Geometry for " + id + " (" + i + ") not supported " + poly.getClass().getSimpleName());
+                }
+                
+                SpatialRule spatialRule = spatialRuleFactory.createSpatialRule(id, borders);
+                if (spatialRule != SpatialRule.EMPTY) {
+                    spatialRules.add(spatialRule);
+                    for (Polygon polygon : spatialRule.getBorders()) {
+                        envelope.expandToInclude(polygon.getEnvelopeInternal());
+                    }
                 }
             }
         }
 
-        if (!polygonBounds.isValid()) {
+        Envelope calculatedBounds = envelope.intersection(maxBBox);
+        if (calculatedBounds.isNull())
             return SpatialRuleLookup.EMPTY;
-        }
 
-        BBox calculatedBounds = polygonBounds.calculateIntersection(maxBBox);
-        if (calculatedBounds == null)
-            return SpatialRuleLookup.EMPTY;
 
-        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupArray(spatialRules, resolution, true, calculatedBounds);
+        // ensure that the spatial ID is stable
+        Collections.sort(spatialRules, SPATIAL_ID_COMP);
+        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupJTS(spatialRules, calculatedBounds);
 
-        logger.info("Created the SpatialRuleLookup with the following rules: " + Arrays.toString(spatialRules.toArray()));
+        logger.info("Created the SpatialRuleLookup with the following rules: {}", Arrays.toString(spatialRules.toArray()));
 
         return spatialRuleLookup;
     }
 
     /**
-     * Wrapper Method for {@link SpatialRuleLookupBuilder#buildIndex(JsonFeatureCollection, String, SpatialRuleFactory, double, BBox)}.
+     * Wrapper Method for {@link SpatialRuleLookupBuilder#buildIndex(List, String, SpatialRuleFactory, Envelope)}.
      * This method simply passes a world-wide BBox, this won't limit the SpatialRuleLookup.
      */
-    public static SpatialRuleLookup buildIndex(JsonFeatureCollection jsonFeatureCollection, String jsonIdField, SpatialRuleFactory spatialRuleFactory) {
-        return buildIndex(jsonFeatureCollection, jsonIdField, spatialRuleFactory, .1, new BBox(-180, 180, -90, 90));
+    public static SpatialRuleLookup buildIndex(List<JsonFeatureCollection> jsonFeatureCollections, String jsonIdField, SpatialRuleFactory spatialRuleFactory) {
+        return buildIndex(jsonFeatureCollections, jsonIdField, spatialRuleFactory, new Envelope(-180, 180, -90, 90));
     }
 }
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupHelper.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupHelper.java
index c6aee6412..f657e93b2 100644
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupHelper.java
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupHelper.java
@@ -24,7 +24,10 @@ import com.graphhopper.routing.util.parsers.TagParserFactory;
 import com.graphhopper.routing.util.parsers.SpatialRuleParser;
 import com.graphhopper.routing.util.parsers.TagParser;
 import com.graphhopper.util.PMap;
-import com.graphhopper.util.shapes.BBox;
+
+import java.util.List;
+
+import org.locationtech.jts.geom.Envelope;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -38,9 +41,9 @@ public class SpatialRuleLookupHelper {
 
     private static final Logger logger = LoggerFactory.getLogger(SpatialRuleLookupHelper.class);
 
-    public static void buildAndInjectSpatialRuleIntoGH(GraphHopper graphHopper, BBox maxBounds, JsonFeatureCollection jsonFeatureCollection) {
-        final SpatialRuleLookup index = SpatialRuleLookupBuilder.buildIndex(jsonFeatureCollection, "ISO_A3", new CountriesSpatialRuleFactory(), .1, maxBounds);
-        logger.info("Set spatial rule lookup with " + index.size() + " rules");
+    public static void buildAndInjectSpatialRuleIntoGH(GraphHopper graphHopper, Envelope maxBounds, List<JsonFeatureCollection> jsonFeatureCollections) {
+        final SpatialRuleLookup index = SpatialRuleLookupBuilder.buildIndex(jsonFeatureCollections, "ISO_A3", new CountriesSpatialRuleFactory(), maxBounds);
+        logger.info("Set spatial rule lookup with {} rules", index.size());
         final TagParserFactory oldTPF = graphHopper.getTagParserFactory();
         graphHopper.setTagParserFactory(new TagParserFactory() {
 
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupJTS.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupJTS.java
new file mode 100644
index 000000000..1165280ad
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupJTS.java
@@ -0,0 +1,140 @@
+/*
+ *  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.spatialrules;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+import org.locationtech.jts.index.strtree.STRtree;
+
+import com.graphhopper.util.shapes.GHPoint;
+
+/**
+ * @author Thomas Butz
+ */
+public class SpatialRuleLookupJTS implements SpatialRuleLookup {
+    
+    private final GeometryFactory geometryFactory = new GeometryFactory();
+    private final List<SpatialRule> rules;
+    private final Envelope maxBounds;
+    private final STRtree index;
+    
+    public SpatialRuleLookupJTS(List<SpatialRule> spatialRules, Envelope maxBounds) {
+        this.index = new STRtree();
+        
+        Map<Polygon, SpatialRuleContainer> containerMap = new HashMap<>();
+        Set<String> ruleIDs = new HashSet<>();
+        for (SpatialRule rule : spatialRules) {
+            if (rule == null)
+                throw new IllegalArgumentException("rule cannot be null");
+
+            if (rule.equals(SpatialRule.EMPTY))
+                throw new IllegalArgumentException("rule cannot be EMPTY");
+            
+            if (!ruleIDs.add(rule.getId()))
+                throw new IllegalArgumentException("Duplicate rule ID: \"" + rule.getId() + "\"");
+            
+            for (Polygon border : rule.getBorders()) {
+                Envelope borderEnvelope = border.getEnvelopeInternal();
+                if (!maxBounds.intersects(borderEnvelope)) {
+                    continue;
+                }
+                
+                SpatialRuleContainer container = containerMap.get(border);
+                if (container == null) {
+                    container = new SpatialRuleContainer(border);
+                    containerMap.put(border, container);
+                    index.insert(borderEnvelope, container);
+                }
+                container.addRule(rule);
+            }
+        }
+
+        index.build();
+
+        this.rules = new ArrayList<>();
+        rules.add(SpatialRule.EMPTY);
+        rules.addAll(spatialRules);
+        this.maxBounds = maxBounds;
+    }
+
+    @Override
+    public SpatialRule lookupRule(double lat, double lon) {
+        if (!maxBounds.covers(lon, lat)) {
+            return SpatialRule.EMPTY;
+        }
+
+        Envelope searchEnv = new Envelope(lon, lon, lat, lat);
+        
+        @SuppressWarnings("unchecked")
+        List<SpatialRuleContainer> containers = index.query(searchEnv);
+        
+        if (containers.isEmpty()) {
+            return SpatialRule.EMPTY;
+        }
+        
+        Point point = geometryFactory.createPoint(new Coordinate(lon, lat));
+        List<SpatialRule> applicableRules = new ArrayList<>();
+        for (SpatialRuleContainer container : containers) {
+            if (container.containsProperly(point)) {
+                applicableRules.addAll(container.getRules());
+            }
+        }
+        
+        if (applicableRules.isEmpty()) {
+            return SpatialRule.EMPTY;
+        }
+
+        //TODO support multiple rules
+        return applicableRules.get(0);
+    }
+
+    @Override
+    public SpatialRule lookupRule(GHPoint point) {
+        return lookupRule(point.lat, point.lon);
+    }
+    
+    @Override
+    public int getSpatialId(SpatialRule rule) {
+        return rules.indexOf(rule);
+    }
+    
+    @Override
+    public SpatialRule getSpatialRule(int spatialId) {
+        return rules.get(spatialId);
+    }
+
+    @Override
+    public int size() {
+        return rules.size();
+    }
+
+    @Override
+    public Envelope getBounds() {
+        return maxBounds;
+    }
+}
diff --git a/docs/core/spatial-rules.md b/docs/core/spatial-rules.md
index 45db8386f..96d604296 100644
--- a/docs/core/spatial-rules.md
+++ b/docs/core/spatial-rules.md
@@ -12,8 +12,7 @@ I you have a working GraphHopper setup it is easy to enable Spatial Rules, **but
 We provide a set of approximate country borders, within the GraphHopper repository. If you need exact borders you can
 get the exact borders from [here](https://github.com/datasets/geo-countries). Go to your
 `config.yml` file and
-uncommend the line: `spatial_rules.location` and point it to where your rules are. You need to re-import your graph after 
-that.
+uncommend the line: `spatial_rules.borders_directory` and point it to the directory where your rules are. You need to re-import your graph after that.
 
 ## Creating Rules
 
diff --git a/reader-osm/src/main/java/com/graphhopper/reader/osm/GraphHopperOSM.java b/reader-osm/src/main/java/com/graphhopper/reader/osm/GraphHopperOSM.java
index 0f985a597..8680861cb 100644
--- a/reader-osm/src/main/java/com/graphhopper/reader/osm/GraphHopperOSM.java
+++ b/reader-osm/src/main/java/com/graphhopper/reader/osm/GraphHopperOSM.java
@@ -23,10 +23,12 @@ import com.graphhopper.reader.DataReader;
 import com.graphhopper.routing.lm.PrepareLandmarks;
 import com.graphhopper.routing.util.spatialrules.*;
 import com.graphhopper.storage.GraphHopperStorage;
-import com.graphhopper.util.shapes.Polygon;
 
+import java.util.Collections;
 import java.util.List;
 
+import org.locationtech.jts.geom.Polygon;
+
 /**
  * This class is the simplified entry to all functionality if you import from OpenStreetMap data.
  *
@@ -69,7 +71,7 @@ public class GraphHopperOSM extends GraphHopper {
             return;
 
         if (landmarkSplittingFeatureCollection != null && !landmarkSplittingFeatureCollection.getFeatures().isEmpty()) {
-            SpatialRuleLookup ruleLookup = SpatialRuleLookupBuilder.buildIndex(landmarkSplittingFeatureCollection, "area", new SpatialRuleLookupBuilder.SpatialRuleFactory() {
+            SpatialRuleLookup ruleLookup = SpatialRuleLookupBuilder.buildIndex(Collections.singletonList(landmarkSplittingFeatureCollection), "area", new SpatialRuleLookupBuilder.SpatialRuleFactory() {
                 @Override
                 public SpatialRule createSpatialRule(final String id, List<Polygon> polygons) {
                     return new DefaultSpatialRule() {
diff --git a/web-bundle/src/main/java/com/graphhopper/http/GraphHopperManaged.java b/web-bundle/src/main/java/com/graphhopper/http/GraphHopperManaged.java
index 8d890c37d..3d2207093 100644
--- a/web-bundle/src/main/java/com/graphhopper/http/GraphHopperManaged.java
+++ b/web-bundle/src/main/java/com/graphhopper/http/GraphHopperManaged.java
@@ -30,13 +30,23 @@ import com.graphhopper.util.CmdArgs;
 import com.graphhopper.util.Parameters;
 import com.graphhopper.util.shapes.BBox;
 import io.dropwizard.lifecycle.Managed;
+
+import org.locationtech.jts.geom.Envelope;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.BufferedReader;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
 
 import static com.graphhopper.util.Helper.UTF_CS;
 
@@ -61,15 +71,25 @@ public class GraphHopperManaged implements Managed {
         } else {
             graphHopper = new GraphHopperOSM(landmarkSplittingFeatureCollection).forServer();
         }
-        String spatialRuleLocation = configuration.get("spatial_rules.location", "");
-        if (!spatialRuleLocation.isEmpty()) {
+        if (!configuration.get("spatial_rules.location", "").isEmpty()) {
+            throw new RuntimeException("spatial_rules.location has been deprecated. Please use spatial_rules.borders_directory instead.");
+        }
+        String spatialRuleBordersDirLocation = configuration.get("spatial_rules.borders_directory", "");
+        if (!spatialRuleBordersDirLocation.isEmpty()) {
             final BBox maxBounds = BBox.parseBBoxString(configuration.get("spatial_rules.max_bbox", "-180, 180, -90, 90"));
-            try (final InputStreamReader reader = new InputStreamReader(new FileInputStream(spatialRuleLocation), UTF_CS)) {
-                JsonFeatureCollection jsonFeatureCollection = localObjectMapper.readValue(reader, JsonFeatureCollection.class);
-                SpatialRuleLookupHelper.buildAndInjectSpatialRuleIntoGH(graphHopper, maxBounds, jsonFeatureCollection);
+            final Path bordersDirectory = Paths.get(spatialRuleBordersDirLocation);
+            List<JsonFeatureCollection> jsonFeatureCollections = new ArrayList<>();
+            try (DirectoryStream<Path> stream = Files.newDirectoryStream(bordersDirectory, "*.{geojson,json}")) {
+                for (Path borderFile : stream) {
+                    try (BufferedReader reader = Files.newBufferedReader(borderFile, StandardCharsets.UTF_8)) {
+                        JsonFeatureCollection jsonFeatureCollection = localObjectMapper.readValue(reader, JsonFeatureCollection.class);
+                        jsonFeatureCollections.add(jsonFeatureCollection);
+                    }
+                }
             } catch (IOException e) {
                 throw new RuntimeException(e);
             }
+            SpatialRuleLookupHelper.buildAndInjectSpatialRuleIntoGH(graphHopper, new Envelope(maxBounds.minLon, maxBounds.maxLon, maxBounds.minLat, maxBounds.maxLat), jsonFeatureCollections);
         }
         graphHopper.init(configuration);
     }
@@ -77,10 +97,10 @@ public class GraphHopperManaged implements Managed {
     @Override
     public void start() {
         graphHopper.importOrLoad();
-        logger.info("loaded graph at:" + graphHopper.getGraphHopperLocation()
-                + ", data_reader_file:" + graphHopper.getDataReaderFile()
-                + ", encoded values:" + graphHopper.getEncodingManager().toEncodedValuesAsString()
-                + ", " + graphHopper.getGraphHopperStorage().toDetailsString());
+        logger.info("loaded graph at:{}, data_reader_file:{}, encoded values:{}, {}",
+                        graphHopper.getGraphHopperLocation(), graphHopper.getDataReaderFile(),
+                        graphHopper.getEncodingManager().toEncodedValuesAsString(),
+                        graphHopper.getGraphHopperStorage().toDetailsString());
     }
 
     public GraphHopper getGraphHopper() {


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/lm/LandmarkStorageTest.java b/core/src/test/java/com/graphhopper/routing/lm/LandmarkStorageTest.java
index 12e9d9fa5..b1e63e2c5 100644
--- a/core/src/test/java/com/graphhopper/routing/lm/LandmarkStorageTest.java
+++ b/core/src/test/java/com/graphhopper/routing/lm/LandmarkStorageTest.java
@@ -31,11 +31,11 @@ import com.graphhopper.storage.GraphHopperStorage;
 import com.graphhopper.storage.RAMDirectory;
 import com.graphhopper.util.EdgeIteratorState;
 import com.graphhopper.util.GHUtility;
-import com.graphhopper.util.shapes.BBox;
 import com.graphhopper.util.shapes.GHPoint;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.locationtech.jts.geom.Envelope;
 
 import java.util.Arrays;
 
@@ -229,8 +229,8 @@ public class LandmarkStorageTest {
             }
 
             @Override
-            public BBox getBounds() {
-                return new BBox(-180, 180, -90, 90);
+            public Envelope getBounds() {
+                return new Envelope(-180d, 180d, -90d, 90d);
             }
         };
 
diff --git a/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupBuilderTest.java b/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupBuilderTest.java
index 8d9f3dd52..ce644981e 100644
--- a/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupBuilderTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupBuilderTest.java
@@ -23,22 +23,27 @@ import com.graphhopper.reader.ReaderWay;
 import com.graphhopper.routing.profiles.*;
 import com.graphhopper.routing.util.CarFlagEncoder;
 import com.graphhopper.routing.util.EncodingManager;
-import com.graphhopper.routing.util.FlagEncoder;
 import com.graphhopper.routing.util.parsers.SpatialRuleParser;
+import com.graphhopper.routing.util.spatialrules.SpatialRuleLookupBuilder.SpatialRuleFactory;
 import com.graphhopper.routing.util.spatialrules.countries.GermanySpatialRule;
 import com.graphhopper.storage.Graph;
 import com.graphhopper.storage.GraphBuilder;
 import com.graphhopper.storage.IntsRef;
 import com.graphhopper.util.EdgeIteratorState;
 import com.graphhopper.util.PMap;
-import com.graphhopper.util.shapes.BBox;
 import com.graphhopper.util.shapes.GHPoint;
-import com.graphhopper.util.shapes.Polygon;
 import org.junit.Test;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Polygon;
 
 import java.io.FileReader;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import static com.graphhopper.util.GHUtility.updateDistancesFor;
 import static junit.framework.TestCase.assertFalse;
@@ -50,12 +55,13 @@ import static org.junit.Assert.assertEquals;
  */
 public class SpatialRuleLookupBuilderTest {
 
+    private static final long MAX_BENCHMARK_RUNTIME_MS = 10_000L;
     private static final String COUNTRIES_FILE = "../core/files/spatialrules/countries.geo.json";
 
     @Test
     public void testIndex() throws IOException {
         final FileReader reader = new FileReader(COUNTRIES_FILE);
-        SpatialRuleLookup spatialRuleLookup = SpatialRuleLookupBuilder.buildIndex(Jackson.newObjectMapper().readValue(reader, JsonFeatureCollection.class), "ISO_A3", new CountriesSpatialRuleFactory());
+        SpatialRuleLookup spatialRuleLookup = SpatialRuleLookupBuilder.buildIndex(Collections.singletonList(Jackson.newObjectMapper().readValue(reader, JsonFeatureCollection.class)), "ISO_A3", new CountriesSpatialRuleFactory());
 
         // Berlin
         assertEquals(RoadAccess.DESTINATION, spatialRuleLookup.lookupRule(52.5243700, 13.4105300).getAccess("track", TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
@@ -74,8 +80,8 @@ public class SpatialRuleLookupBuilderTest {
     @Test
     public void testBounds() throws IOException {
         final FileReader reader = new FileReader(COUNTRIES_FILE);
-        SpatialRuleLookup spatialRuleLookup = SpatialRuleLookupBuilder.buildIndex(Jackson.newObjectMapper().readValue(reader, JsonFeatureCollection.class), "ISO_A3", new CountriesSpatialRuleFactory(), .1, new BBox(-180, 180, -90, 90));
-        BBox almostWorldWide = new BBox(-179, 179, -89, 89);
+        SpatialRuleLookup spatialRuleLookup = SpatialRuleLookupBuilder.buildIndex(Collections.singletonList(Jackson.newObjectMapper().readValue(reader, JsonFeatureCollection.class)), "ISO_A3", new CountriesSpatialRuleFactory());
+        Envelope almostWorldWide = new Envelope(-179, 179, -89, 89);
 
         // Might fail if a polygon is defined outside the above coordinates
         assertTrue("BBox seems to be not contracted", almostWorldWide.contains(spatialRuleLookup.getBounds()));
@@ -88,28 +94,30 @@ public class SpatialRuleLookupBuilderTest {
          So the BBox should not contain a Point lying somewhere close in Germany.
         */
         final FileReader reader = new FileReader(COUNTRIES_FILE);
-        SpatialRuleLookup spatialRuleLookup = SpatialRuleLookupBuilder.buildIndex(Jackson.newObjectMapper().readValue(reader, JsonFeatureCollection.class), "ISO_A3", new CountriesSpatialRuleFactory(), .1, new BBox(9, 10, 51, 52));
+        SpatialRuleLookup spatialRuleLookup = SpatialRuleLookupBuilder.buildIndex(Collections.singletonList(Jackson.newObjectMapper().readValue(reader, JsonFeatureCollection.class)), "ISO_A3", new CountriesSpatialRuleFactory(), new Envelope(9, 10, 51, 52));
         assertFalse("BBox seems to be incorrectly contracted", spatialRuleLookup.getBounds().contains(49.9, 8.9));
     }
 
     @Test
     public void testNoIntersection() throws IOException {
         final FileReader reader = new FileReader(COUNTRIES_FILE);
-        SpatialRuleLookup spatialRuleLookup = SpatialRuleLookupBuilder.buildIndex(Jackson.newObjectMapper().readValue(reader, JsonFeatureCollection.class), "ISO_A3", new CountriesSpatialRuleFactory(), .1, new BBox(-180, -179, -90, -89));
+        SpatialRuleLookup spatialRuleLookup = SpatialRuleLookupBuilder.buildIndex(Collections.singletonList(Jackson.newObjectMapper().readValue(reader, JsonFeatureCollection.class)), "ISO_A3", new CountriesSpatialRuleFactory(), new Envelope(-180, -179, -90, -89));
         assertEquals(SpatialRuleLookup.EMPTY, spatialRuleLookup);
     }
 
 
     @Test
     public void testSpatialId() {
+        final GeometryFactory fac = new GeometryFactory();
+        org.locationtech.jts.geom.Polygon polygon = fac.createPolygon(new Coordinate[] { new Coordinate(0, 0), new Coordinate(0, 1), new Coordinate(1, 1), new Coordinate(1, 0), new Coordinate(0, 0) });
         final GermanySpatialRule germany = new GermanySpatialRule();
-        germany.setBorders(Collections.singletonList(new Polygon(new double[]{0, 0, 1, 1}, new double[]{0, 1, 1, 0})));
+        germany.setBorders(Collections.singletonList(polygon));
 
         SpatialRuleLookup index = new SpatialRuleLookup() {
             @Override
             public SpatialRule lookupRule(double lat, double lon) {
                 for (Polygon polygon : germany.getBorders()) {
-                    if (polygon.contains(lat, lon)) {
+                    if (polygon.covers(fac.createPoint(new Coordinate(lon, lat)))) {
                         return germany;
                     }
                 }
@@ -141,8 +149,8 @@ public class SpatialRuleLookupBuilderTest {
             }
 
             @Override
-            public BBox getBounds() {
-                return new BBox(-180, 180, -90, 90);
+            public Envelope getBounds() {
+                return new Envelope(-180, 180, -90, 90);
             }
         };
 
@@ -191,4 +199,60 @@ public class SpatialRuleLookupBuilderTest {
         e4.setFlags(em.handleWayTags(livingStreet2, map, relFlags));
         assertEquals(MaxSpeed.UNSET_SPEED, e4.get(tmpCarMaxSpeedEnc), .1);
     }
+
+    @Test
+    public void testSpeed() throws IOException {
+        final FileReader reader = new FileReader(COUNTRIES_FILE);
+        SpatialRuleFactory rulePerCountryFactory = new SpatialRuleFactory() {
+            
+            @Override
+            public SpatialRule createSpatialRule(final String id, final List<Polygon> borders) {
+                return new SpatialRule() {
+                    
+                    @Override
+                    public double getMaxSpeed(String highway, double _default) {
+                        return 100;
+                    }
+                    
+                    @Override
+                    public String getId() {
+                        return id;
+                    }
+                    
+                    @Override
+                    public List<Polygon> getBorders() {
+                        return borders;
+                    }
+                    
+                    @Override
+                    public RoadAccess getAccess(String highwayTag, TransportationMode transportationMode, RoadAccess _default) {
+                        return RoadAccess.YES;
+                    }
+                    
+                    @Override
+                    public String toString() {
+                        return getId();
+                    }
+                };
+            }
+        };
+        SpatialRuleLookup spatialRuleLookup = SpatialRuleLookupBuilder.buildIndex(Collections.singletonList(Jackson.newObjectMapper().readValue(reader, JsonFeatureCollection.class)), "ISO_A3", rulePerCountryFactory);
+    
+        // generate random points in central Europe
+        List<GHPoint> randomPoints = new ArrayList<>();
+        for (int i = 0; i < 1_000_000; i++) {
+            double lat = 46d + Math.random() * 7d;
+            double lon = 6d + Math.random() * 21d;
+            randomPoints.add(new GHPoint(lat, lon));
+        }
+        
+        long start = System.nanoTime();
+        for (GHPoint point : randomPoints) {
+            spatialRuleLookup.lookupRule(point);
+        }
+        long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+        System.out.println("Lookup of " + randomPoints.size() + " points took " + duration + "ms");
+        System.out.println("Average lookup duration: " + ((double) duration) / randomPoints.size() + "ms");
+        assertTrue("Benchmark must be finished in less than " + MAX_BENCHMARK_RUNTIME_MS + "ms", duration < MAX_BENCHMARK_RUNTIME_MS);
+    }
 }
diff --git a/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupArrayTest.java b/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupJTSTest.java
similarity index 57%
rename from core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupArrayTest.java
rename to core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupJTSTest.java
index 9aa6cabc8..69fbfcdce 100644
--- a/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupArrayTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupJTSTest.java
@@ -1,9 +1,12 @@
 package com.graphhopper.routing.util.spatialrules;
 
 import com.graphhopper.routing.profiles.RoadAccess;
-import com.graphhopper.util.shapes.BBox;
-import com.graphhopper.util.shapes.Polygon;
 import org.junit.Test;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.Polygon;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -13,29 +16,35 @@ import static org.junit.Assert.assertTrue;
 
 /**
  * @author Robin Boldt
+ * @author Thomas Butz
  */
-public class SpatialRuleLookupArrayTest {
+public class SpatialRuleLookupJTSTest {
+    
+    private static final GeometryFactory FAC = new GeometryFactory();
 
     @Test
     public void testSpatialLookup() {
+        Polygon deBorder = FAC.createPolygon(new Coordinate[] { new Coordinate(1, 1), new Coordinate(2, 1), new Coordinate(2, 2), new Coordinate(1, 2), new Coordinate(1, 1) });
         List<SpatialRule> spatialRules = new ArrayList<>();
         SpatialRule germany = new DefaultSpatialRule() {
             @Override
             public String getId() {
                 return "DEU";
             }
-        }.addBorder(new Polygon(new double[]{1, 1, 2, 2}, new double[]{1, 2, 2, 1}));
+        }.addBorder(deBorder);
         spatialRules.add(germany);
+        
+        Polygon atBorder = FAC.createPolygon(new Coordinate[] { new Coordinate(5, 5), new Coordinate(6, 5), new Coordinate(6, 6), new Coordinate(5, 6), new Coordinate(5, 5) });
         SpatialRule austria = new DefaultSpatialRule() {
             @Override
             public String getId() {
                 return "AUT";
             }
-        }.addBorder(new Polygon(new double[]{5, 5, 6, 6}, new double[]{5, 6, 6, 5}));
+        }.addBorder(atBorder);
         spatialRules.add(austria);
 
         // create lookup with bbox just for DEU (for space reduction)
-        SpatialRuleLookupArray lookup = new SpatialRuleLookupArray(spatialRules, 1, false, new BBox(1, 2, 1, 2));
+        SpatialRuleLookupJTS lookup = new SpatialRuleLookupJTS(spatialRules, deBorder.getEnvelopeInternal());
         SpatialRule rule = lookup.lookupRule(1.5, 1.5);
         assertEquals(germany, rule);
         assertEquals("DEU", rule.getId());
@@ -45,38 +54,7 @@ public class SpatialRuleLookupArrayTest {
     }
 
     @Test
-    public void testSmallScenario() {
-        List<SpatialRule> spatialRules = new ArrayList<>();
-        spatialRules.add(getSpatialRule(new Polygon(new double[]{1, 1, 2, 2}, new double[]{1, 2, 2, 1}), "1"));
-        spatialRules.add(getSpatialRule(new Polygon(new double[]{1, 1, 3.6, 3.6}, new double[]{3, 4, 4, 3}), "2"));
-
-        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupArray(spatialRules, 1, false, new BBox(1, 4, 1, 4));
-
-        assertEquals(RoadAccess.DESTINATION, spatialRuleLookup.lookupRule(1.2, 1.7).getAccess(null, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
-        assertEquals(RoadAccess.DESTINATION, spatialRuleLookup.lookupRule(1.2, 3.7).getAccess(null, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
-        assertEquals(RoadAccess.YES, spatialRuleLookup.lookupRule(2.2, 1.7).getAccess(null, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
-    }
-
-    @Test
-    public void testExact() {
-        List<SpatialRule> spatialRules = new ArrayList<>();
-        spatialRules.add(getSpatialRule(new Polygon(new double[]{1, 1, 2, 2}, new double[]{1, 2, 2, 1}), "1"));
-        spatialRules.add(getSpatialRule(new Polygon(new double[]{1, 1, 3.6, 3.6}, new double[]{3, 4, 4, 3}), "2"));
-        spatialRules.add(getSpatialRule(new Polygon(new double[]{1, 1, 2, 2}, new double[]{-1, 0, 0, -1}), "3"));
-
-        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupArray(spatialRules, 1, true, new BBox(-1, 4, 1, 4));
-
-        assertEquals(RoadAccess.DESTINATION, spatialRuleLookup.lookupRule(1.2, 1.7).getAccess(null, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
-        assertEquals(RoadAccess.DESTINATION, spatialRuleLookup.lookupRule(1.2, 3.7).getAccess(null, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
-        // Not in the second Polygon anymore
-        assertEquals(RoadAccess.YES, spatialRuleLookup.lookupRule(3.9, 3.7).getAccess(null, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
-        assertEquals(RoadAccess.YES, spatialRuleLookup.lookupRule(2.2, 1.7).getAccess(null, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
-        // Get the EmptySpatialRule in a BorderTile #1077
-        assertEquals(SpatialRule.EMPTY.getId(), spatialRuleLookup.lookupRule(0.9, 0.9).getId());
-    }
-
-    @Test
-    public void testExactCountry() {
+    public void testPrecision() {
         List<SpatialRule> spatialRules = new ArrayList<>();
 
         // Taken from here: https://github.com/johan/world.geo.json/blob/master/countries/DEU.geo.json
@@ -89,7 +67,7 @@ public class SpatialRuleLookupArrayTest {
                 return "DEU";
             }
         }.addBorder(germanPolygon));
-        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupArray(spatialRules, .1, true, new BBox(-180, 180, -90, 90));
+        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupJTS(spatialRules, new Envelope(-180, 180, -90, 90));
 
         // Far from the border of Germany, in Germany
         assertEquals("DEU", spatialRuleLookup.lookupRule(48.777106, 9.180769).getId());
@@ -113,37 +91,40 @@ public class SpatialRuleLookupArrayTest {
         assertEquals("SpatialRule.EMPTY", spatialRuleLookup.lookupRule(49.932900, 6.174023).getId());
         assertEquals("SpatialRule.EMPTY", spatialRuleLookup.lookupRule(47.547463, 9.741948).getId());
     }
-
+    
     @Test
-    public void testExactAdjacentBorder() {
-        List<SpatialRule> spatialRules = new ArrayList<>();
-        // Two rules that divide the tile in half
-        spatialRules.add(getSpatialRule(new Polygon(new double[]{1, 1, 1.5, 1.5}, new double[]{1, 2, 2, 1}), "top"));
-        spatialRules.add(getSpatialRule(new Polygon(new double[]{1.5, 1.5, 2, 2}, new double[]{1, 2, 2, 1}), "bottom"));
+    public void testHole() {
+        LinearRing shell = FAC.createLinearRing(new Coordinate[] { new Coordinate(1, 1), new Coordinate(7, 1), new Coordinate(7, 7), new Coordinate(1, 7), new Coordinate(1, 1)});
+        LinearRing hole = FAC.createLinearRing(new Coordinate[] { new Coordinate(4, 2), new Coordinate(6, 2), new Coordinate(6, 4), new Coordinate(4, 6), new Coordinate(4, 2)});
+        Polygon p1 = FAC.createPolygon(shell, new LinearRing[] { hole });
 
-        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupArray(spatialRules, 1, true, new BBox(1, 4, 1, 4));
-
-        assertEquals("top", spatialRuleLookup.lookupRule(1.4, 1.5).getId());
-        assertEquals("bottom", spatialRuleLookup.lookupRule(1.6, 1.5).getId());
-    }
-
-    @Test
-    public void testSmallBoundsBigPolygon() {
         List<SpatialRule> spatialRules = new ArrayList<>();
-        spatialRules.add(getSpatialRule(new Polygon(new double[]{-100, -100, 100, 100}, new double[]{-100, 100, 100, -100}), "big"));
-        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupArray(spatialRules, 1, true, new BBox(1, 2, 1, 2));
-        assertEquals("big", spatialRuleLookup.lookupRule(1.5, 1.5).getId());
+        spatialRules.add(getSpatialRule(p1, "1"));
+        
+        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupJTS(spatialRules, p1.getEnvelopeInternal());
+        
+        assertEquals("SpatialRule.EMPTY", spatialRuleLookup.lookupRule(3, 5).getId());
+
+        Polygon p2 = FAC.createPolygon(hole);
+        spatialRules.add(getSpatialRule(p2, "2"));
+        spatialRuleLookup = new SpatialRuleLookupJTS(spatialRules, p1.getEnvelopeInternal());
+        
+        assertEquals("2", spatialRuleLookup.lookupRule(3, 5).getId());
     }
 
     @Test
     public void testSpatialRuleForId() {
         List<SpatialRule> spatialRules = new ArrayList<>();
-        spatialRules.add(getSpatialRule(new Polygon(new double[]{1, 1, 1.5, 1.5}, new double[]{1, 2, 2, 1}), "1"));
-        spatialRules.add(getSpatialRule(new Polygon(new double[]{1.5, 1.5, 2, 2}, new double[]{1, 2, 2, 1}), "2"));
-        spatialRules.add(getSpatialRule(new Polygon(new double[]{1.5, 1.5, 2, 2}, new double[]{1, 2, 2, 1}), "3"));
-        spatialRules.add(getSpatialRule(new Polygon(new double[]{1.5, 1.5, 2, 2}, new double[]{1, 2, 2, 1}), "4"));
+        Polygon p1 = FAC.createPolygon(new Coordinate[] { new Coordinate(1, 1), new Coordinate(2, 1), new Coordinate(2, 1.5), new Coordinate(1, 1.5), new Coordinate(1, 1)});
+        Polygon p2 = FAC.createPolygon(new Coordinate[] { new Coordinate(1, 1.5), new Coordinate(2, 1.5), new Coordinate(2, 2), new Coordinate(1, 2), new Coordinate(1, 1.5)});
+        Polygon p3 = FAC.createPolygon(new Coordinate[] { new Coordinate(1, 1.5), new Coordinate(2, 1.5), new Coordinate(2, 2), new Coordinate(1, 2), new Coordinate(1, 1.5)});
+        Polygon p4 = FAC.createPolygon(new Coordinate[] { new Coordinate(1, 1.5), new Coordinate(2, 1.5), new Coordinate(2, 2), new Coordinate(1, 2), new Coordinate(1, 1.5)});
+        spatialRules.add(getSpatialRule(p1, "1"));
+        spatialRules.add(getSpatialRule(p2, "2"));
+        spatialRules.add(getSpatialRule(p3, "3"));
+        spatialRules.add(getSpatialRule(p4, "4"));
 
-        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupArray(spatialRules, .1, false, new BBox(1, 2, 1, 2));
+        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupJTS(spatialRules, new Envelope(1, 2, 1, 2));
 
         // Note index=0 is the EMPTY rule
         assertEquals("1", spatialRuleLookup.getSpatialRule(1).getId());
@@ -152,18 +133,17 @@ public class SpatialRuleLookupArrayTest {
 
     private Polygon parsePolygonString(String polygonString) {
         String[] germanPolygonArr = polygonString.split("\\],\\[");
-        double[] lats = new double[germanPolygonArr.length];
-        double[] lons = new double[germanPolygonArr.length];
+        Coordinate[] shell = new Coordinate[germanPolygonArr.length + 1];
         for (int i = 0; i < germanPolygonArr.length; i++) {
             String temp = germanPolygonArr[i];
             temp = temp.replaceAll("\\[", "");
             temp = temp.replaceAll("\\]", "");
             String[] coords = temp.split(",");
-            lats[i] = Double.parseDouble(coords[1]);
-            lons[i] = Double.parseDouble(coords[0]);
+            shell[i] = new Coordinate(Double.parseDouble(coords[0]), Double.parseDouble(coords[1]));
         }
+        shell[shell.length - 1] = shell[0];
 
-        return new Polygon(lats, lons);
+        return FAC.createPolygon(shell);
     }
 
     private SpatialRule getSpatialRule(Polygon p, final String name) {
diff --git a/web/src/test/java/com/graphhopper/http/SpatialRulesTest.java b/web/src/test/java/com/graphhopper/http/SpatialRulesTest.java
index efcffd3cf..b1c6bc2c9 100644
--- a/web/src/test/java/com/graphhopper/http/SpatialRulesTest.java
+++ b/web/src/test/java/com/graphhopper/http/SpatialRulesTest.java
@@ -41,13 +41,13 @@ public class SpatialRulesTest {
     private static final GraphHopperServerConfiguration config = new GraphHopperServerConfiguration();
 
     static {
-        // The EncodedValue "country" requires the setting "spatial_rules.location" as "country" does not load via DefaultTagParserFactory
+     // The EncodedValue "country" requires the setting "spatial_rules.borders_directory" as "country" does not load via DefaultTagParserFactory
         // TODO should we automatically detect this somehow and include a default country file?
         config.getGraphHopperConfiguration().merge(new CmdArgs().
                 put("graph.flag_encoders", "car").
                 put("graph.encoded_values", "country,road_environment,road_class,road_access,max_speed").
                 put("prepare.ch.weightings", "no").
-                put("spatial_rules.location", "../core/files/spatialrules/countries.geo.json").
+                put("spatial_rules.borders_directory", "../core/files/spatialrules").
                 put("spatial_rules.max_bbox", "11.4,11.7,49.9,50.1").
                 put("datareader.file", "../core/files/north-bayreuth.osm.gz").
                 put("graph.location", DIR));