Repository: graphhopper/graphhopper
Pull request #1903 (2020-02-12 10:33:14)

Allow overlapping SpatialRules with priorities
This PR builds on top of https://github.com/graphhopper/graphhopper/pull/1880 and restructures the `SpatialRuleLookup` interface to return `SpatialRuleSets` which cover multiple rules.

otbutz: @karussell any feedback so far?
otbutz: > Thanks! Did you do some performance tests how this affects speed if spatial rules are present? 

I'll test it with an import but the `SpatialRuleSet.EMPTY` should already avoid a lot of unnecessary object creation. The sorting overhead in `lookupRules()` shouldn't be that bad considering that the number of overlapping rules for a certain point is going to be very low.

The next step after this PR will be to store the active rule IDs in the graph properties in order to drop the Country enum. That should remove the last obstacle for external rule files :)
karussell: > I'll test it with an import 

Thanks!

> The next step after this PR will be to store the active rule IDs in the graph properties in order to drop the Country enum. That should remove the last obstacle for external rule files :)

But why a special handling for the country? E.g. for road_class we also use the enum for storage. This sounds a bit strange but as we also want enums for a nicer/simpler Java API there is IMO no better solution.
otbutz: > but as we also want enums for a nicer/simpler Java API there is IMO no better solution.

The user will be able to pass arbitrary rules with custom IDs later. I cannot use enums for this.
karussell: Ok. I think we have two requirements here: one is a set of countries (Country enum) and the other are custom areas where we could use integer IDs or even the StringIndex. So even if we make custom areas possible without code we cannot remove the Country enum.
otbutz: Is there a reason why we should treat country rules differently? We could also move them to external files later and use the same mechanism.
karussell: Have not a strong opinion about this but "country" is a distinct set of values similar to what we have for road_class.
otbutz: I've tested the import with a small dataset(Bavaria) and didn't really notice a difference. But i'm going to run the import using the Europe pbf just to be sure.
otbutz: @karussell anything missing?
karussell: Sorry for the delay and yes, time is missing ;)

Have already asked @boldtrn to try this out in real world, so we'll have some progress here within the next 3 weeks or so :)
boldtrn: > Have not a strong opinion about this but "country" is a distinct set of values similar to what we have for road_class.

I think the biggest advantage of using ids is that it's easy to extend and customize. For example one might use countries most of the time, but in the US, we might use states, because rules can differ a lot between states (for example - I have not idea if this is true). Enums are a bit unflexible and hard to change. But with IDs I don't care, I can simply add them.
boldtrn: I had a look at the code and I missed a test case for the overlapping, so I wrote one for `SpatialRuleLookupJTSTest` to also better understand the internals, maybe we could add it (or anything similar) to the PR?

```java
    @Test
    public void testOverlapWitPriority() {
        // Let the two polygons overlap
        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 AbstractSpatialRule(deBorder) {
            @Override
            public String getId() {
                return "DEU";
            }
        };
        spatialRules.add(germany);

        Polygon atBorder = FAC.createPolygon(new Coordinate[] { new Coordinate(0.5, 1), new Coordinate(1.5, 1), new Coordinate(1.5, 2), new Coordinate(0.5, 2), new Coordinate(0.5, 1) });
        SpatialRule austria = new AbstractSpatialRule(atBorder) {
            @Override
            public String getId() {
                return "AUT";
            }

            @Override
            public int getPriority(){
                return 500;
            }
        };
        spatialRules.add(austria);

        // create lookup with bbox just for DEU (for space reduction)
        SpatialRuleLookupJTS lookup = new SpatialRuleLookupJTS(spatialRules, new Envelope(0, 3, 0, 3));
        SpatialRuleSet set = lookup.lookupRules(1.5, 1.25);
        assertEquals(2, set.getRules().size());
        // Since Austria is priority 500, Germany should be the first, Austria the second
        assertEquals(germany.getId(), set.getRules().get(0).getId());
        assertEquals(austria.getId(), set.getRules().get(1).getId());
        assertEquals(1, lookup.lookupRules(1.5, 0.99).getRules().size());
        assertEquals(2, lookup.lookupRules(1.5, 1.00).getRules().size());
        assertEquals(2, lookup.lookupRules(1.5, 1.50).getRules().size());
        assertEquals(1, lookup.lookupRules(1.5, 1.51).getRules().size());
    }
``` 
otbutz: @boldtrn Thank you! I have added your test case and listed you as commit author.
boldtrn: Thanks! The author wouldn't have been necessary :)
boldtrn: I also did a manual test by changing the polygons of Germany and Austria, so that they massively overlap and checked if the priorities and overlapping is resolved correctly. The results look good to me.

![Screenshot from 2020-04-14 16-57-26](https://user-images.githubusercontent.com/1553525/79239652-1bbd0c80-7e71-11ea-9fda-6de3cbf27996.png)
![Screenshot from 2020-04-14 16-57-09](https://user-images.githubusercontent.com/1553525/79239644-19f34900-7e71-11ea-8b37-32a45a997513.png)
 
otbutz: > A minor thing I missed was to specify the priority for example through the geojson, but I think this could be easily added in the future, if that's a desired change in the future.

This is going to be one of the next steps:

* storing IDs as part of the graph properties
* defining an external rule file
boldtrn: > This is going to be one of the next steps

I thought so :). So I think this should be good to merge.
karussell: Thanks @otbutz and @boldtrn !

Patch:
diff --git a/core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java b/core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java
index 10d74b1f0..ed240397a 100644
--- a/core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java
+++ b/core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java
@@ -258,7 +258,7 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
         IntHashSet blockedEdges = new IntHashSet();
 
         // the ruleLookup splits certain areas from each other but avoids making this a permanent change so that other algorithms still can route through these regions.
-        if (ruleLookup != null && ruleLookup.size() > 0) {
+        if (ruleLookup != null && !ruleLookup.getRules().isEmpty()) {
             StopWatch sw = new StopWatch().start();
             blockedEdges = findBorderEdgeIds(ruleLookup);
             tarjanFilter = new BlockedEdgesFilter(encoder.getAccessEnc(), true, false, blockedEdges);
@@ -293,7 +293,7 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
                     GHPoint p = createPoint(graph, nextStartNode);
                     if (logDetails)
                         LOGGER.info("start node: " + nextStartNode + " (" + p + ") subnetwork size: " + subnetworkIds.size()
-                                + ", " + Helper.getMemInfo() + ((ruleLookup == null) ? "" : " area:" + ruleLookup.lookupRule(p).getId()));
+                                + ", " + Helper.getMemInfo() + ((ruleLookup == null) ? "" : " area:" + ruleLookup.lookupRules(p.lat, p.lon).getRules()));
 
                     if (createLandmarksForSubnetwork(nextStartNode, subnetworks, blockedEdges))
                         break;
@@ -464,10 +464,10 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
         IntHashSet inaccessible = new IntHashSet();
         while (allEdgesIterator.next()) {
             int adjNode = allEdgesIterator.getAdjNode();
-            SpatialRule ruleAdj = ruleLookup.lookupRule(nodeAccess.getLatitude(adjNode), nodeAccess.getLongitude(adjNode));
+            SpatialRule ruleAdj = ruleLookup.lookupRules(nodeAccess.getLatitude(adjNode), nodeAccess.getLongitude(adjNode)).getRules().get(0);
 
             int baseNode = allEdgesIterator.getBaseNode();
-            SpatialRule ruleBase = ruleLookup.lookupRule(nodeAccess.getLatitude(baseNode), nodeAccess.getLongitude(baseNode));
+            SpatialRule ruleBase = ruleLookup.lookupRules(nodeAccess.getLatitude(baseNode), nodeAccess.getLongitude(baseNode)).getRules().get(0);
             if (ruleAdj != ruleBase) {
                 inaccessible.add(allEdgesIterator.getEdge());
             }
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxSpeedParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxSpeedParser.java
index de08833ee..351e3cc85 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxSpeedParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMaxSpeedParser.java
@@ -24,7 +24,7 @@ import com.graphhopper.routing.profiles.EncodedValueLookup;
 import com.graphhopper.routing.profiles.MaxSpeed;
 import com.graphhopper.routing.profiles.RoadClass;
 import com.graphhopper.routing.util.AbstractFlagEncoder;
-import com.graphhopper.routing.util.spatialrules.SpatialRule;
+import com.graphhopper.routing.util.spatialrules.SpatialRuleSet;
 import com.graphhopper.routing.util.spatialrules.TransportationMode;
 import com.graphhopper.storage.IntsRef;
 
@@ -56,12 +56,12 @@ public class OSMMaxSpeedParser implements TagParser {
     public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay way, boolean ferry, IntsRef relationFlags) {
         double maxSpeed = AbstractFlagEncoder.parseSpeed(way.getTag("maxspeed"));
 
-        SpatialRule spatialRule = way.getTag("spatial_rule", null);
-        if (spatialRule != null) {
+        SpatialRuleSet spatialRuleSet = way.getTag("spatial_rule_set", null);
+        if (spatialRuleSet != null && spatialRuleSet != SpatialRuleSet.EMPTY) {
             RoadClass roadClass = RoadClass.find(way.getTag("highway", ""));
-            maxSpeed = spatialRule.getMaxSpeed(roadClass, TransportationMode.MOTOR_VEHICLE, maxSpeed);
+            maxSpeed = spatialRuleSet.getMaxSpeed(roadClass, TransportationMode.MOTOR_VEHICLE, maxSpeed);
         }
-            
+
         double fwdSpeed = AbstractFlagEncoder.parseSpeed(way.getTag("maxspeed:forward"));
         if (fwdSpeed < 0 && maxSpeed > 0)
             fwdSpeed = maxSpeed;
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMRoadAccessParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMRoadAccessParser.java
index 32b4094fe..976b0a61d 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMRoadAccessParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMRoadAccessParser.java
@@ -19,7 +19,7 @@ package com.graphhopper.routing.util.parsers;
 
 import com.graphhopper.reader.ReaderWay;
 import com.graphhopper.routing.profiles.*;
-import com.graphhopper.routing.util.spatialrules.SpatialRule;
+import com.graphhopper.routing.util.spatialrules.SpatialRuleSet;
 import com.graphhopper.routing.util.spatialrules.TransportationMode;
 import com.graphhopper.storage.IntsRef;
 
@@ -57,10 +57,12 @@ public class OSMRoadAccessParser implements TagParser {
             }
         }
 
-        SpatialRule spatialRule = readerWay.getTag("spatial_rule", null);
-        if (spatialRule != null)
-            accessValue = spatialRule.getAccess(RoadClass.find(readerWay.getTag("highway", "")), TransportationMode.MOTOR_VEHICLE, YES);
-
+        SpatialRuleSet spatialRuleSet = readerWay.getTag("spatial_rule_set", null);
+        if (spatialRuleSet != null && spatialRuleSet != SpatialRuleSet.EMPTY) {
+            RoadClass roadClass = RoadClass.find(readerWay.getTag("highway", ""));
+            accessValue = spatialRuleSet.getAccess(roadClass, TransportationMode.MOTOR_VEHICLE, YES);
+        }
+            
         roadAccessEnc.setEnum(false, edgeFlags, accessValue);
         return edgeFlags;
     }
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/SpatialRuleParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/SpatialRuleParser.java
index b44947ad6..e9446ff03 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/SpatialRuleParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/SpatialRuleParser.java
@@ -21,8 +21,8 @@ import com.graphhopper.reader.ReaderWay;
 import com.graphhopper.routing.profiles.EncodedValue;
 import com.graphhopper.routing.profiles.EncodedValueLookup;
 import com.graphhopper.routing.profiles.IntEncodedValue;
-import com.graphhopper.routing.util.spatialrules.SpatialRule;
 import com.graphhopper.routing.util.spatialrules.SpatialRuleLookup;
+import com.graphhopper.routing.util.spatialrules.SpatialRuleSet;
 import com.graphhopper.storage.IntsRef;
 import com.graphhopper.util.shapes.GHPoint;
 
@@ -52,9 +52,9 @@ public class SpatialRuleParser implements TagParser {
     public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay way, boolean ferry, IntsRef relationFlags) {
         GHPoint estimatedCenter = way.getTag("estimated_center", null);
         if (estimatedCenter != null) {
-            SpatialRule rule = spatialRuleLookup.lookupRule(estimatedCenter);
-            way.setTag("spatial_rule", rule);
-            spatialRuleEnc.setInt(false, edgeFlags, spatialRuleLookup.getSpatialId(rule));
+            SpatialRuleSet ruleSet = spatialRuleLookup.lookupRules(estimatedCenter.lat, estimatedCenter.lon);
+            way.setTag("spatial_rule_set", ruleSet);
+            spatialRuleEnc.setInt(false, edgeFlags, ruleSet.getSpatialId());
         }
         return edgeFlags;
     }
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 423f7dde3..80bd79bd9 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
@@ -29,6 +29,8 @@ import com.graphhopper.routing.profiles.RoadClass;
  * @author Robin Boldt
  */
 public abstract class AbstractSpatialRule implements SpatialRule {
+    
+    public static final int DEFAULT_PRIORITY = 100;
 
     private final List<Polygon> borders;
     
@@ -54,6 +56,11 @@ public abstract class AbstractSpatialRule implements SpatialRule {
         return borders;
     }
 
+    @Override
+    public int getPriority() {
+        return DEFAULT_PRIORITY;
+    }
+    
     @Override
     public boolean equals(Object obj) {
         if (this == obj) {
@@ -72,6 +79,12 @@ public abstract class AbstractSpatialRule implements SpatialRule {
 
     @Override
     public String toString() {
-        return getId();
+        StringBuilder builder = new StringBuilder();
+        builder.append("SpatialRule [getId()=");
+        builder.append(getId());
+        builder.append(", getPriority()=");
+        builder.append(getPriority());
+        builder.append("]");
+        return builder.toString();
     }
 }
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 425743657..634709972 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
@@ -24,6 +24,6 @@ public class CountriesSpatialRuleFactory implements SpatialRuleLookupBuilder.Spa
         } catch (Exception ex) {
         }
 
-        return SpatialRule.EMPTY;
+        return null;
     }
 }
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 afb0b047a..2e32f5cd2 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
@@ -21,7 +21,6 @@ import com.graphhopper.routing.profiles.RoadAccess;
 import com.graphhopper.routing.profiles.RoadClass;
 
 import java.util.List;
-import java.util.Objects;
 
 import org.locationtech.jts.geom.Polygon;
 
@@ -60,50 +59,18 @@ public interface SpatialRule {
      */
     List<Polygon> getBorders();
 
+    /**
+     * Returns the priority of the rule. If multiple rules overlap they're
+     * processed in natural order of their priority.
+     * 
+     * @return the priority as an integer value between
+     *         {@link Integer#MIN_VALUE} (minimum priority) and
+     *         {@link Integer#MAX_VALUE} (maximum priority)
+     */
+    int getPriority();
+
     /**
      * Returns the id for this rule, e.g. the ISO name of the country. The id has to be unique.
      */
     String getId();
-
-
-    SpatialRule EMPTY = new SpatialRule() {
-        @Override
-        public double getMaxSpeed(RoadClass roadClass, TransportationMode transport, double currentMaxSpeed) {
-            return currentMaxSpeed;
-        }
-
-        @Override
-        public RoadAccess getAccess(RoadClass roadClass, TransportationMode transport, RoadAccess currentRoadAccess) {
-            return currentRoadAccess;
-        }
-
-        // should we use Country.DEFAULT here?
-        @Override
-        public String getId() {
-            return "SpatialRule.EMPTY";
-        }
-
-        @Override
-        public List<Polygon> getBorders() {
-            throw new IllegalArgumentException("Empty rule does not have borders");
-        }
-
-        @Override
-        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/SpatialRuleLookup.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookup.java
index 213c26d9d..875311fbe 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,49 +17,35 @@
  */
 package com.graphhopper.routing.util.spatialrules;
 
-import org.locationtech.jts.geom.Envelope;
+import java.util.Collections;
+import java.util.List;
 
-import com.graphhopper.util.shapes.GHPoint;
+import org.locationtech.jts.geom.Envelope;
 
 /**
  * SpatialRuleLookup defines a container that stores SpatialRules and can lookup
- * a SpatialRule rule depending on the location.
+ * applicable rules depending on the location.
  *
  * @author Robin Boldt
  */
 public interface SpatialRuleLookup {
-
+    
     /**
-     * Return an applicable rule for this location.
-     * If there is more than one rule for the location, the implementation can decide which rule to return.
-     * For example this might be the first occurring or the most relevant, see the implementation JavaDoc.
+     * Return applicable rules for this location.
      * <p>
-     * If the requested location is outside of the supported bounds or no SpatialRule is registered at this location
-     * {@link SpatialRule#EMPTY} is returned.
-     */
-    SpatialRule lookupRule(double lat, double lon);
-
-    /**
-     * See {@link #lookupRule(double, double)} for details.
-     */
-    SpatialRule lookupRule(GHPoint point);
-
-    /**
-     * This method returns an identification number from 0 to size (exclusive) for the specified rule.
-     * The id is fix for a given set of SpatialRules.
-     */
-    int getSpatialId(SpatialRule rule);
-
-    /**
-     * This method returns the SpatialRule for a given Spatial Id. This can be used when retrieving SpatialRules from
-     * a Spatial Id stored in the graph.
+     * If multiple rules with the same priority overlap for a location, the
+     * implementation can decide which of them to return and in which order.
+     * <p>
+     * If the requested location is outside of the supported bounds or no
+     * SpatialRule is registered at this location {@link SpatialRuleSet#EMPTY}
+     * is returned.
      */
-    SpatialRule getSpatialRule(int spatialId);
+    SpatialRuleSet lookupRules(double lat, double lon);
 
     /**
-     * @return the number of rules added to this lookup.
+     * @return the rules which are active for {@link #getBounds()}
      */
-    int size();
+    List<SpatialRule> getRules();
 
     /**
      * @return the bounds of the SpatialRuleLookup
@@ -68,28 +54,13 @@ public interface SpatialRuleLookup {
 
     SpatialRuleLookup EMPTY = new SpatialRuleLookup() {
         @Override
-        public SpatialRule lookupRule(double lat, double lon) {
-            return SpatialRule.EMPTY;
-        }
-
-        @Override
-        public SpatialRule lookupRule(GHPoint point) {
-            return SpatialRule.EMPTY;
+        public SpatialRuleSet lookupRules(double lat, double lon) {
+            return SpatialRuleSet.EMPTY;
         }
-
-        @Override
-        public int getSpatialId(SpatialRule rule) {
-            return 0;
-        }
-
-        @Override
-        public SpatialRule getSpatialRule(int spatialId) {
-            return SpatialRule.EMPTY;
-        }
-
+        
         @Override
-        public int size() {
-            return 1;
+        public List<SpatialRule> getRules() {
+            return Collections.emptyList();
         }
 
         @Override
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 39b5af07e..2db65d890 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
@@ -60,7 +60,7 @@ public class SpatialRuleLookupBuilder {
                 }
 
                 SpatialRule spatialRule = spatialRuleFactory.createSpatialRule(Helper.toLowerCase(id), borders);
-                if (spatialRule != SpatialRule.EMPTY) {
+                if (spatialRule != null) {
                     spatialRules.add(spatialRule);
                     for (Polygon polygon : spatialRule.getBorders()) {
                         envelope.expandToInclude(polygon.getEnvelopeInternal());
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 4cf195c6c..27555aa8f 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
@@ -78,7 +78,7 @@ public class SpatialRuleLookupHelper {
         }
         final SpatialRuleLookup index = SpatialRuleLookupBuilder.buildIndex(reorder(jsonFeatureCollections, subset),
                 JSON_ID_FIELD, new CountriesSpatialRuleFactory(), maxBounds);
-        logger.info("Set spatial rule lookup with {} rules", index.size());
+        logger.info("Set spatial rule lookup with {} rules", index.getRules().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
index 127c2b4b8..3e62329d8 100644
--- a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupJTS.java
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupJTS.java
@@ -17,43 +17,54 @@
  */
 package com.graphhopper.routing.util.spatialrules;
 
-import com.graphhopper.util.shapes.GHPoint;
+import java.util.*;
+
 import org.locationtech.jts.geom.*;
 import org.locationtech.jts.index.strtree.STRtree;
 
-import java.util.*;
-
 /**
  * @author Thomas Butz
  */
 public class SpatialRuleLookupJTS implements SpatialRuleLookup {
-
+    
+    private static final Comparator<SpatialRule> RULE_COMP = new Comparator<SpatialRule>() {
+
+        @Override
+        public int compare(SpatialRule o1, SpatialRule o2) {
+            int comp = Integer.compare(o1.getPriority(), o2.getPriority());
+            if (comp != 0) {
+                return comp;
+            }
+            
+            return o1.getId().compareTo(o2.getId());
+        }
+    };
+    
     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<>();
+        List<SpatialRule> registeredRules = new ArrayList<>();
         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() + "\"");
-
+            
+            boolean registered = false;
             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);
@@ -61,32 +72,35 @@ public class SpatialRuleLookupJTS implements SpatialRuleLookup {
                     index.insert(borderEnvelope, container);
                 }
                 container.addRule(rule);
+                registered = true;
+            }
+            
+            if (registered) {
+                registeredRules.add(rule);
             }
         }
 
         index.build();
 
-        this.rules = new ArrayList<>();
-        rules.add(SpatialRule.EMPTY);
-        rules.addAll(spatialRules);
+        this.rules = Collections.unmodifiableList(registeredRules);
         this.maxBounds = maxBounds;
     }
 
     @Override
-    public SpatialRule lookupRule(double lat, double lon) {
+    public SpatialRuleSet lookupRules(double lat, double lon) {
         if (!maxBounds.covers(lon, lat)) {
-            return SpatialRule.EMPTY;
+            return SpatialRuleSet.EMPTY;
         }
 
         Envelope searchEnv = new Envelope(lon, lon, lat, lat);
-
+        
         @SuppressWarnings("unchecked")
         List<SpatialRuleContainer> containers = index.query(searchEnv);
-
+        
         if (containers.isEmpty()) {
-            return SpatialRule.EMPTY;
+            return SpatialRuleSet.EMPTY;
         }
-
+        
         Point point = geometryFactory.createPoint(new Coordinate(lon, lat));
         List<SpatialRule> applicableRules = new ArrayList<>();
         for (SpatialRuleContainer container : containers) {
@@ -94,33 +108,21 @@ public class SpatialRuleLookupJTS implements SpatialRuleLookup {
                 applicableRules.addAll(container.getRules());
             }
         }
-
+        
         if (applicableRules.isEmpty()) {
-            return SpatialRule.EMPTY;
+            return SpatialRuleSet.EMPTY;
         }
-
-        //TODO support multiple rules
-        return applicableRules.get(0);
-    }
-
-    @Override
-    public SpatialRule lookupRule(GHPoint point) {
-        return lookupRule(point.lat, point.lon);
+        
+        Collections.sort(applicableRules, RULE_COMP);
+        
+        int spatialId = rules.indexOf(applicableRules.get(applicableRules.size() - 1)) + 1;
+        
+        return new SpatialRuleSet(applicableRules, spatialId);
     }
-
-    @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();
+    public List<SpatialRule> getRules() {
+        return rules;
     }
 
     @Override
diff --git a/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleSet.java b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleSet.java
new file mode 100644
index 000000000..caa3f2b2c
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/spatialrules/SpatialRuleSet.java
@@ -0,0 +1,101 @@
+/*
+ *  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.Collections;
+import java.util.List;
+
+import com.graphhopper.routing.profiles.RoadAccess;
+import com.graphhopper.routing.profiles.RoadClass;
+
+/**
+ * Contains all rules which are applicable for a certain position.
+ * 
+ * @author Thomas Butz
+ */
+public class SpatialRuleSet {
+    public static final SpatialRuleSet EMPTY = new SpatialRuleSet(Collections.<SpatialRule>emptyList(), 0);
+    
+    private final List<SpatialRule> rules;
+    private final int spatialId;
+
+    /**
+     * @param rules     a List of rules, ordered according to how they are to be applied
+     * @param spatialId the index of the rule with the highest priority
+     */
+    public SpatialRuleSet(List<SpatialRule> rules, int spatialId) {
+        this.rules = Collections.unmodifiableList(rules);
+        this.spatialId = spatialId;
+    }
+    
+    /**
+     * Return the max speed for a certain road class.
+     *
+     * @param roadClass       The highway type, e.g. {@link RoadClass#MOTORWAY}
+     * @param transport       The mode of transportation
+     * @param currentMaxSpeed The current max speed value or -1 if no value has been set yet
+     * @return the maximum speed value to be used
+     */
+    public double getMaxSpeed(RoadClass roadClass, TransportationMode transport, double currentMaxSpeed) {
+        double value = currentMaxSpeed;
+        for (SpatialRule rule : rules) {
+            value = rule.getMaxSpeed(roadClass, transport, value);
+        }
+        return value;
+    }
+
+    /**
+     * Returns the {@link RoadAccess} for a certain highway type and transportation mode.
+     *
+     * @param roadClass          The highway type, e.g. {@link RoadClass#MOTORWAY}
+     * @param transport          The mode of transportation
+     * @param currentRoadAccess  The current road access value (default: {@link RoadAccess#YES})
+     * @return the type of access to be used
+     */
+    public RoadAccess getAccess(RoadClass roadClass, TransportationMode transport, RoadAccess currentRoadAccess) {
+        RoadAccess value = currentRoadAccess;
+        for (SpatialRule rule : rules) {
+            value = rule.getAccess(roadClass, transport, value);
+        }
+        return value;
+    }
+    
+    /**
+     * @return the rules in this set
+     */
+    public List<SpatialRule> getRules() {
+        return rules;
+    }
+
+    /**
+     * @return the id of the rule with the highest priority or
+     *         <i>0</i> if the set doesn't contain any rules
+     */
+    public int getSpatialId() {
+        return spatialId;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("SpatialRuleSet [rules=");
+        builder.append(rules);
+        builder.append("]");
+        return builder.toString();
+    }
+}
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 51e90f984..3c9e42350 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
@@ -89,7 +89,7 @@ public class GraphHopperOSM extends GraphHopper {
                     });
             for (PrepareLandmarks prep : getLMPreparationHandler().getPreparations()) {
                 // the ruleLookup splits certain areas from each other but avoids making this a permanent change so that other algorithms still can route through these regions.
-                if (ruleLookup != null && ruleLookup.size() > 0) {
+                if (ruleLookup != null && !ruleLookup.getRules().isEmpty()) {
                     prep.setSpatialRuleLookup(ruleLookup);
                 }
             }
diff --git a/tools/src/main/java/com/graphhopper/tools/Measurement.java b/tools/src/main/java/com/graphhopper/tools/Measurement.java
index 3219b50e0..505231ce9 100644
--- a/tools/src/main/java/com/graphhopper/tools/Measurement.java
+++ b/tools/src/main/java/com/graphhopper/tools/Measurement.java
@@ -525,7 +525,8 @@ public class Measurement {
         MiniPerfTest lookupPerfTest = new MiniPerfTest() {
             @Override
             public int doCalc(boolean warmup, int run) {
-                return spatialRuleLookup.lookupRule(randomPoints.get(run)).hashCode();
+                GHPoint point = randomPoints.get(run);
+                return spatialRuleLookup.lookupRules(point.lat, point.lon).getRules().size();
             }
         }.setIterations(count).start();
 


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 b599b558a..064595ee9 100644
--- a/core/src/test/java/com/graphhopper/routing/lm/LandmarkStorageTest.java
+++ b/core/src/test/java/com/graphhopper/routing/lm/LandmarkStorageTest.java
@@ -24,6 +24,7 @@ import com.graphhopper.routing.util.FlagEncoder;
 import com.graphhopper.routing.util.spatialrules.AbstractSpatialRule;
 import com.graphhopper.routing.util.spatialrules.SpatialRule;
 import com.graphhopper.routing.util.spatialrules.SpatialRuleLookup;
+import com.graphhopper.routing.util.spatialrules.SpatialRuleSet;
 import com.graphhopper.routing.weighting.FastestWeighting;
 import com.graphhopper.storage.DataAccess;
 import com.graphhopper.storage.Directory;
@@ -40,6 +41,7 @@ import org.locationtech.jts.geom.Polygon;
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 import static org.junit.Assert.*;
 
@@ -201,33 +203,24 @@ public class LandmarkStorageTest {
             }
         };
         final SpatialRuleLookup lookup = new SpatialRuleLookup() {
+            
+            private final List<SpatialRule> rules = Arrays.asList(ruleLeft, ruleRight);
 
             @Override
-            public SpatialRule lookupRule(double lat, double lon) {
-                if (lon > 0.00105)
-                    return ruleRight;
-
-                return ruleLeft;
-            }
-
-            @Override
-            public SpatialRule lookupRule(GHPoint point) {
-                return lookupRule(point.lat, point.lon);
+            public SpatialRuleSet lookupRules(double lat, double lon) {
+                SpatialRule rule;
+                if (lon > 0.00105) {
+                    rule = ruleRight;
+                } else {
+                    rule = ruleLeft;
+                }
+
+                return new SpatialRuleSet(Collections.singletonList(rule), rules.indexOf(rule)+1);
             }
-
-            @Override
-            public int getSpatialId(SpatialRule rule) {
-                throw new IllegalStateException();
-            }
-
-            @Override
-            public SpatialRule getSpatialRule(int spatialId) {
-                throw new IllegalStateException();
-            }
-
+            
             @Override
-            public int size() {
-                return 2;
+            public List<SpatialRule> getRules() {
+                return rules;
             }
 
             @Override
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 28a81b809..46e7ee2e1 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
@@ -40,6 +40,7 @@ import org.locationtech.jts.geom.Polygon;
 import java.io.FileReader;
 import java.io.IOException;
 import java.util.Collections;
+import java.util.List;
 
 import static com.graphhopper.util.GHUtility.updateDistancesFor;
 import static junit.framework.TestCase.assertFalse;
@@ -60,23 +61,23 @@ public class SpatialRuleLookupBuilderTest {
                 Jackson.newObjectMapper().readValue(reader, JsonFeatureCollection.class)), "ISO_A3", new CountriesSpatialRuleFactory());
 
         // Berlin
-        assertEquals(RoadAccess.DESTINATION, spatialRuleLookup.lookupRule(52.5243700, 13.4105300).
+        assertEquals(RoadAccess.DESTINATION, spatialRuleLookup.lookupRules(52.5243700, 13.4105300).
                 getAccess(RoadClass.TRACK, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
-        assertEquals(RoadAccess.YES, spatialRuleLookup.lookupRule(52.5243700, 13.4105300).
+        assertEquals(RoadAccess.YES, spatialRuleLookup.lookupRules(52.5243700, 13.4105300).
                 getAccess(RoadClass.PRIMARY, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
 
         // Paris -> empty rule
-        assertEquals(RoadAccess.YES, spatialRuleLookup.lookupRule(48.864716, 2.349014).
+        assertEquals(RoadAccess.YES, spatialRuleLookup.lookupRules(48.864716, 2.349014).
                 getAccess(RoadClass.TRACK, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
-        assertEquals(RoadAccess.YES, spatialRuleLookup.lookupRule(48.864716, 2.349014).
+        assertEquals(RoadAccess.YES, spatialRuleLookup.lookupRules(48.864716, 2.349014).
                 getAccess(RoadClass.PRIMARY, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
 
         // Austria
-        assertEquals(RoadAccess.FORESTRY, spatialRuleLookup.lookupRule(48.204484, 16.107888).
+        assertEquals(RoadAccess.FORESTRY, spatialRuleLookup.lookupRules(48.204484, 16.107888).
                 getAccess(RoadClass.TRACK, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
-        assertEquals(RoadAccess.YES, spatialRuleLookup.lookupRule(48.210033, 16.363449).
+        assertEquals(RoadAccess.YES, spatialRuleLookup.lookupRules(48.210033, 16.363449).
                 getAccess(RoadClass.PRIMARY, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
-        assertEquals(RoadAccess.DESTINATION, spatialRuleLookup.lookupRule(48.210033, 16.363449).
+        assertEquals(RoadAccess.DESTINATION, spatialRuleLookup.lookupRules(48.210033, 16.363449).
                 getAccess(RoadClass.LIVING_STREET, TransportationMode.MOTOR_VEHICLE, RoadAccess.YES));
     }
 
@@ -119,41 +120,22 @@ public class SpatialRuleLookupBuilderTest {
         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(Collections.singletonList(polygon));
+        final SpatialRule germany = new GermanySpatialRule(Collections.singletonList(polygon));
 
         SpatialRuleLookup index = new SpatialRuleLookup() {
             @Override
-            public SpatialRule lookupRule(double lat, double lon) {
+            public SpatialRuleSet lookupRules(double lat, double lon) {
                 for (Polygon polygon : germany.getBorders()) {
                     if (polygon.covers(fac.createPoint(new Coordinate(lon, lat)))) {
-                        return germany;
+                        return new SpatialRuleSet(Collections.singletonList(germany), 1);
                     }
                 }
-                return SpatialRule.EMPTY;
+                return SpatialRuleSet.EMPTY;
             }
-
-            @Override
-            public SpatialRule lookupRule(GHPoint point) {
-                return lookupRule(point.lat, point.lon);
-            }
-
-            @Override
-            public int getSpatialId(SpatialRule rule) {
-                if (germany.equals(rule)) {
-                    return 1;
-                } else {
-                    return 0;
-                }
-            }
-
-            @Override
-            public SpatialRule getSpatialRule(int spatialId) {
-                return SpatialRule.EMPTY;
-            }
-
+            
             @Override
-            public int size() {
-                return 2;
+            public List<SpatialRule> getRules() {
+                return Collections.singletonList(germany);
             }
 
             @Override
@@ -192,8 +174,8 @@ public class SpatialRuleLookupBuilderTest {
         e2.setFlags(em.handleWayTags(way2, map, relFlags));
         assertEquals(RoadAccess.YES, e2.get(tmpRoadAccessEnc));
 
-        assertEquals(index.getSpatialId(new GermanySpatialRule(Collections.singletonList(polygon))), e1.get(countrySpatialIdEnc));
-        assertEquals(index.getSpatialId(SpatialRule.EMPTY), e2.get(countrySpatialIdEnc));
+        assertEquals(index.getRules().indexOf(germany), e1.get(countrySpatialIdEnc)-1);
+        assertEquals(0, e2.get(countrySpatialIdEnc));
 
         ReaderWay livingStreet = new ReaderWay(29L);
         livingStreet.setTag("highway", "living_street");
diff --git a/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupJTSTest.java b/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupJTSTest.java
index dda374e5a..05f1f4684 100644
--- a/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupJTSTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/spatialrules/SpatialRuleLookupJTSTest.java
@@ -11,9 +11,11 @@ import org.locationtech.jts.geom.LinearRing;
 import org.locationtech.jts.geom.Polygon;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
 /**
@@ -47,51 +49,49 @@ public class SpatialRuleLookupJTSTest {
 
         // create lookup with bbox just for DEU (for space reduction)
         SpatialRuleLookupJTS lookup = new SpatialRuleLookupJTS(spatialRules, deBorder.getEnvelopeInternal());
-        SpatialRule rule = lookup.lookupRule(1.5, 1.5);
-        assertEquals(germany, rule);
+        SpatialRule rule = getFirstRule(lookup, 1.5, 1.5);
+        assertSame(germany, rule);
         assertEquals("DEU", rule.getId());
-        int id = lookup.getSpatialId(rule);
-        assertTrue(id > 0);
-        assertEquals(rule, lookup.getSpatialRule(id));
+        int id = lookup.getRules().indexOf(rule);
+        assertTrue(id > -1);
+        assertEquals(rule, lookup.getRules().get(id));
     }
 
     @Test
     public void testPrecision() {
-        List<SpatialRule> spatialRules = new ArrayList<>();
-
         // Taken from here: https://github.com/johan/world.geo.json/blob/master/countries/DEU.geo.json
         String germanPolygonJson = "[9.921906,54.983104],[9.93958,54.596642],[10.950112,54.363607],[10.939467,54.008693],[11.956252,54.196486],[12.51844,54.470371],[13.647467,54.075511],[14.119686,53.757029],[14.353315,53.248171],[14.074521,52.981263],[14.4376,52.62485],[14.685026,52.089947],[14.607098,51.745188],[15.016996,51.106674],[14.570718,51.002339],[14.307013,51.117268],[14.056228,50.926918],[13.338132,50.733234],[12.966837,50.484076],[12.240111,50.266338],[12.415191,49.969121],[12.521024,49.547415],[13.031329,49.307068],[13.595946,48.877172],[13.243357,48.416115],[12.884103,48.289146],[13.025851,47.637584],[12.932627,47.467646],[12.62076,47.672388],[12.141357,47.703083],[11.426414,47.523766],[10.544504,47.566399],[10.402084,47.302488],[9.896068,47.580197],[9.594226,47.525058],[8.522612,47.830828],[8.317301,47.61358],[7.466759,47.620582],[7.593676,48.333019],[8.099279,49.017784],[6.65823,49.201958],[6.18632,49.463803],[6.242751,49.902226],[6.043073,50.128052],[6.156658,50.803721],[5.988658,51.851616],[6.589397,51.852029],[6.84287,52.22844],[7.092053,53.144043],[6.90514,53.482162],[7.100425,53.693932],[7.936239,53.748296],[8.121706,53.527792],[8.800734,54.020786],[8.572118,54.395646],[8.526229,54.962744],[9.282049,54.830865],[9.921906,54.983104]";
         Polygon germanPolygon = parsePolygonString(germanPolygonJson);
-
-        spatialRules.add(new AbstractSpatialRule(germanPolygon) {
+        SpatialRule germanySpatialRule = new AbstractSpatialRule(germanPolygon) {
             @Override
             public String getId() {
                 return "DEU";
             }
-        });
-        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupJTS(spatialRules, new Envelope(-180, 180, -90, 90));
+        };
+
+        SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupJTS(Collections.singletonList(germanySpatialRule), new Envelope(-180, 180, -90, 90));
 
         // Far from the border of Germany, in Germany
-        assertEquals("DEU", spatialRuleLookup.lookupRule(48.777106, 9.180769).getId());
-        assertEquals("DEU", spatialRuleLookup.lookupRule(51.806281, 7.269380).getId());
-        assertEquals("DEU", spatialRuleLookup.lookupRule(50.636710, 12.514561).getId());
+        assertSame(germanySpatialRule, getFirstRule(spatialRuleLookup, 48.777106, 9.180769));
+        assertSame(germanySpatialRule, getFirstRule(spatialRuleLookup, 51.806281, 7.269380));
+        assertSame(germanySpatialRule, getFirstRule(spatialRuleLookup, 50.636710, 12.514561));
 
         // Far from the border of Germany, not in Germany
-        assertEquals("SpatialRule.EMPTY", spatialRuleLookup.lookupRule(48.029533, 7.250122).getId());
-        assertEquals("SpatialRule.EMPTY", spatialRuleLookup.lookupRule(51.694467, 15.209218).getId());
-        assertEquals("SpatialRule.EMPTY", spatialRuleLookup.lookupRule(47.283669, 11.167381).getId());
+        assertSame(null, getFirstRule(spatialRuleLookup, 48.029533, 7.250122));
+        assertSame(null, getFirstRule(spatialRuleLookup, 51.694467, 15.209218));
+        assertSame(null, getFirstRule(spatialRuleLookup, 47.283669, 11.167381));
 
         // Close to the border of Germany, in Germany - Whereas the borders are defined by the GeoJson above and do not strictly follow the acutal border
-        assertEquals("DEU", spatialRuleLookup.lookupRule(50.017714, 12.356129).getId());
-        assertEquals("DEU", spatialRuleLookup.lookupRule(49.949930, 6.225853).getId());
-        assertEquals("DEU", spatialRuleLookup.lookupRule(47.580866, 9.707582).getId());
-        assertEquals("DEU", spatialRuleLookup.lookupRule(47.565101, 9.724267).getId());
-        assertEquals("DEU", spatialRuleLookup.lookupRule(47.557166, 9.738343).getId());
+        assertSame(germanySpatialRule, getFirstRule(spatialRuleLookup, 50.017714, 12.356129));
+        assertSame(germanySpatialRule, getFirstRule(spatialRuleLookup, 49.949930, 6.225853));
+        assertSame(germanySpatialRule, getFirstRule(spatialRuleLookup, 47.580866, 9.707582));
+        assertSame(germanySpatialRule, getFirstRule(spatialRuleLookup, 47.565101, 9.724267));
+        assertSame(germanySpatialRule, getFirstRule(spatialRuleLookup, 47.557166, 9.738343));
 
         // Close to the border of Germany, not in Germany
-        assertEquals("SpatialRule.EMPTY", spatialRuleLookup.lookupRule(50.025342, 12.386262).getId());
-        assertEquals("SpatialRule.EMPTY", spatialRuleLookup.lookupRule(49.932900, 6.174023).getId());
-        assertEquals("SpatialRule.EMPTY", spatialRuleLookup.lookupRule(47.547463, 9.741948).getId());
+        assertSame(null, getFirstRule(spatialRuleLookup, 50.025342, 12.386262));
+        assertSame(null, getFirstRule(spatialRuleLookup, 49.932900, 6.174023));
+        assertSame(null, getFirstRule(spatialRuleLookup, 47.547463, 9.741948));
     }
     
     @Test
@@ -99,19 +99,20 @@ public class SpatialRuleLookupJTSTest {
         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 });
-
+        SpatialRule s1 = getSpatialRule(p1, "1");
         List<SpatialRule> spatialRules = new ArrayList<>();
-        spatialRules.add(getSpatialRule(p1, "1"));
+        spatialRules.add(s1);
         
         SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupJTS(spatialRules, p1.getEnvelopeInternal());
         
-        assertEquals("SpatialRule.EMPTY", spatialRuleLookup.lookupRule(3, 5).getId());
+        assertSame(null, getFirstRule(spatialRuleLookup, 3, 5));
 
         Polygon p2 = FAC.createPolygon(hole);
-        spatialRules.add(getSpatialRule(p2, "2"));
+        SpatialRule s2 = getSpatialRule(p2, "2");
+        spatialRules.add(s2);
         spatialRuleLookup = new SpatialRuleLookupJTS(spatialRules, p1.getEnvelopeInternal());
         
-        assertEquals("2", spatialRuleLookup.lookupRule(3, 5).getId());
+        assertSame(s2, getFirstRule(spatialRuleLookup, 3, 5));
     }
 
     @Test
@@ -121,16 +122,60 @@ public class SpatialRuleLookupJTSTest {
         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"));
+        spatialRules.add(getSpatialRule(p1, "0"));
+        spatialRules.add(getSpatialRule(p2, "1"));
+        spatialRules.add(getSpatialRule(p3, "2"));
+        spatialRules.add(getSpatialRule(p4, "3"));
 
         SpatialRuleLookup spatialRuleLookup = new SpatialRuleLookupJTS(spatialRules, new Envelope(1, 2, 1, 2));
 
-        // Note index=0 is the EMPTY rule
-        assertEquals("1", spatialRuleLookup.getSpatialRule(1).getId());
-        assertEquals("4", spatialRuleLookup.getSpatialRule(4).getId());
+        assertEquals("0", spatialRuleLookup.getRules().get(0).getId());
+        assertEquals("3", spatialRuleLookup.getRules().get(3).getId());
+    }
+    
+    @Test
+    public void testOverlapWithPriority() {
+        // Let the two polygons overlap
+        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 AbstractSpatialRule(deBorder) {
+            @Override
+            public String getId() {
+                return "DEU";
+            }
+        };
+        spatialRules.add(germany);
+
+        Polygon atBorder = FAC.createPolygon(new Coordinate[] { new Coordinate(0.5, 1),
+                        new Coordinate(1.5, 1), new Coordinate(1.5, 2), new Coordinate(0.5, 2),
+                        new Coordinate(0.5, 1) });
+        SpatialRule austria = new AbstractSpatialRule(atBorder) {
+            @Override
+            public String getId() {
+                return "AUT";
+            }
+
+            @Override
+            public int getPriority() {
+                return 500;
+            }
+        };
+        spatialRules.add(austria);
+
+        SpatialRuleLookupJTS lookup = new SpatialRuleLookupJTS(spatialRules,
+                        new Envelope(0, 3, 0, 3));
+        SpatialRuleSet set = lookup.lookupRules(1.5, 1.25);
+        assertEquals(2, set.getRules().size());
+        // Since Austria is priority 500, Germany should be the first, Austria
+        // the second
+        assertEquals(germany.getId(), set.getRules().get(0).getId());
+        assertEquals(austria.getId(), set.getRules().get(1).getId());
+        assertEquals(1, lookup.lookupRules(1.5, 0.99).getRules().size());
+        assertEquals(2, lookup.lookupRules(1.5, 1.00).getRules().size());
+        assertEquals(2, lookup.lookupRules(1.5, 1.50).getRules().size());
+        assertEquals(1, lookup.lookupRules(1.5, 1.51).getRules().size());
     }
 
     private Polygon parsePolygonString(String polygonString) {
@@ -167,4 +212,12 @@ public class SpatialRuleLookupJTSTest {
             }
         };
     }
+    
+    private static SpatialRule getFirstRule(SpatialRuleLookup spatialRuleLookup, double lat, double lon) {
+        SpatialRuleSet set = spatialRuleLookup.lookupRules(lat, lon);
+        if (set.getRules().isEmpty()) {
+            return null;
+        }
+        return set.getRules().get(0);
+    }
 }