Repository: GIScience/openrouteservice
Pull request #2047: feat: add access = permit to known access restiction types
Author: login: merydian
Labels: name: feature
Created: 2025-04-25T12:54:52Z | Merged: 2025-04-29T10:37:23Z

### Pull Request Checklist
<!--- Please make sure you have completed the following items BEFORE submitting a pull request (put an x in each box
when you have checked you have done them): -->
- [x] 1. I have [**rebased**][rebase] the latest version of the main branch into my feature branch and all conflicts
         have been resolved.
- [x] 2. I have added information about the change/addition to functionality to the CHANGELOG.md file under the
         [Unreleased] heading.
- [x] 3. I have documented my code using JDocs tags.
- [x] 4. I have removed unnecessary commented out code, imports and System.out.println statements.
- [x] 5. I have written JUnit tests for any new methods/classes and ensured that they pass.
- [x] 6. I have created API tests for any new functionality exposed to the API.
- [x] 7. If changes/additions are made to the ors-config.json file, I have added these to the [ors config documentation][config]
         along with a short description of what it is for, and documented this in the Pull Request (below).
- [x] 8. I have built graphs with my code of the Heidelberg.osm.gz file and run the api-tests with all test passing
- [x] 9. I have referenced the Issue Number in the Pull Request (if the changes were from an issue).
- [x] 10. For new features or changes involving building of graphs, I have tested on a larger dataset
          (at least Germany), and the graphs build without problems (i.e. no out-of-memory errors).
- [x] 11. For new features or changes involving the graphbuilding process (i.e. changing encoders, updating the
          importer etc.), I have generated longer distance routes for the affected profiles with different options
          (avoid features, max weight etc.) and compared these with the routes of the same parameters and start/end
          points generated from the current live ORS.
          If there are differences then the reasoning for these **MUST** be documented in the pull request.
- [x] 12. I have written in the Pull Request information about the changes made including their intended usage
          and why the change was needed.
- [x] 13. For changes touching the API documentation, I have tested that the API playground [renders correctly][api].

Fixes #2013.

### Information about the changes
- Key functionality added: Access restrictions with the type "permit" will now be considered during graph building. 
- Reason for change:

### Examples and reasons for differences between live ORS routes, and those generated from this pull request
-

### Required changes to ors config (if applicable)
-

[config]: https://GIScience.github.io/openrouteservice/run-instance/configuration/
[api]: https://gitlab.gistools.geog.uni-heidelberg.de/giscience/openrouteservice-infrastructure/ors-docs-api#test-new-ors-documentation
[rebase]: https://github.com/GIScience/openrouteservice/blob/main/CONTRIBUTE.md#pull-request-guidelines


Linked issue #2013: Add `access = permit` to known access restiction types
### Feature/Enhancement Proposal

Information about restricted usage for roads with access restriction of type [permit](https://wiki.openstreetmap.org/wiki/Tag:access%3Dpermit) is not reported because the restriction is missing from the list of known [restriction types](https://github.com/GIScience/openrouteservice/blob/f401ccf0a345384f310b8c39305d19c05243fc85/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/AccessRestrictionType.java).

As an example, consider [Eternitstraße](https://www.openstreetmap.org/way/176768688) in Heidelberg.

![Image](https://github.com/user-attachments/assets/a348d460-6aba-4e6b-aecc-c45756c8e8c0)

Google Maps correctly reports it as restricted usage road.

![Image](https://github.com/user-attachments/assets/696b376b-c175-49ce-8ba1-2ed954e84945)


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7558636333..09ecc02cbf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,7 @@ Releasing is documented in RELEASE.md
 
 ### Added
 - add hint to use yaml editor in docs ([#1836](https://github.com/GIScience/openrouteservice/issues/1836))
+- access = permit as known access restriction type ([#2013](https://github.com/GIScience/openrouteservice/issues/2013))
 
 ### Changed
 
diff --git a/docs/api-reference/endpoints/directions/extra-info/road-access-restrictions.md b/docs/api-reference/endpoints/directions/extra-info/road-access-restrictions.md
index 14d66d9c27..c655911e9e 100644
--- a/docs/api-reference/endpoints/directions/extra-info/road-access-restrictions.md
+++ b/docs/api-reference/endpoints/directions/extra-info/road-access-restrictions.md
@@ -9,14 +9,15 @@ $.routes[*].extras.roadaccessrestrictions.values
 Provides information about possible restrictions on roads.
 Explanation of the values can be found in the [list of possible values in the OSM Wiki](https://wiki.openstreetmap.org/wiki/Key:access)
 
-| Value |             Encoding              |
-|:-----:|:---------------------------------:|
-|   0   | None (there are no restrictions)  |
-|   1   |                No                 |
-|   2   |             Customers             |
-|   4   |            Destination            |
-|   8   |             Delivery              |
-|  16   |              Private              |
-|  32   |            Permissive             |
+| Value |             Encoding             |
+|:-----:|:--------------------------------:|
+|   0   | None (there are no restrictions) |
+|   1   |                No                |
+|   2   |            Customers             |
+|   4   |           Destination            |
+|   8   |             Delivery             |
+|  16   |             Private              |
+|  32   |            Permissive            |
+|  64   |              Permit              |
 
 [//]: # (keep in sync with org.heigit.ors.routing.graphhopper.extensions.AccessRestrictionType.class)
\ No newline at end of file
diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/AccessRestrictionType.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/AccessRestrictionType.java
index fce3b8f57b..f35653e9ac 100644
--- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/AccessRestrictionType.java
+++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/AccessRestrictionType.java
@@ -24,6 +24,7 @@ public abstract class AccessRestrictionType {
     public static final int DELIVERY = 8;
     public static final int PRIVATE = 16;
     public static final int PERMISSIVE = 32;
+    public static final int PERMIT = 64;
 
     private AccessRestrictionType() {
     }
diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilder.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilder.java
index bf5387292d..b5cd68b88e 100644
--- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilder.java
+++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilder.java
@@ -61,6 +61,8 @@ public RoadAccessRestrictionsGraphStorageBuilder() {
         restrictedValues.add("customers");
         restrictedValues.add("emergency");
         restrictedValues.add("permissive");
+        restrictedValues.add("delivery");
+        restrictedValues.add("permit");
 
         permissiveValues.add("yes");
         permissiveValues.add("designated");
@@ -76,9 +78,9 @@ public RoadAccessRestrictionsGraphStorageBuilder() {
      * @return The RoadAccessRestrictionStorage object created as part of the initialisation
      * @throws Exception
      */
-    public GraphExtension init(GraphHopper graphhopper, int profileType) throws Exception {
+    public GraphExtension init(GraphHopper graphhopper, int profileType) throws IllegalStateException {
         if (storage != null)
-            throw new Exception("GraphStorageBuilder has been already initialized.");
+            throw new IllegalStateException("GraphStorageBuilder has been already initialized.");
 
         ExtendedStorageProperties parameters;
         parameters = this.parameters;
@@ -100,9 +102,9 @@ public GraphExtension init(GraphHopper graphhopper, int profileType) throws Exce
      * @return The RoadAccessRestrictionStorage object created as part of the initialisation
      * @throws Exception
      */
-    public GraphExtension init(GraphHopper graphhopper) throws Exception {
+    public GraphExtension init(GraphHopper graphhopper) throws IllegalStateException {
         if (storage != null)
-            throw new Exception("GraphStorageBuilder has been already initialized.");
+            throw new IllegalStateException("GraphStorageBuilder has been already initialized.");
 
         ExtendedStorageProperties parameters;
         parameters = this.parameters;
@@ -166,6 +168,8 @@ public void processWay(ReaderWay way, Coordinate[] coords, Map<Integer, Map<Stri
                 restrictions = isAccessAllowed(way, VAL_BICYCLE) ? 0 : getRestrictionType(way, VAL_BICYCLE);
             if (RoutingProfileType.isPedestrian(profileType))
                 restrictions = isAccessAllowed(way, "foot") ? 0 : getRestrictionType(way, "foot");
+            if (profileType == RoutingProfileType.UNKNOWN)
+                restrictions = getRestrictionType(way, VAL_ACCESS);
         }
     }
 
@@ -230,6 +234,7 @@ private int updateRestriction(int encodedRestrictions, String restrictionValue)
                 case "permissive" -> res |= AccessRestrictionType.PERMISSIVE;
                 case "delivery" -> res |= AccessRestrictionType.DELIVERY;
                 case "customers" -> res |= AccessRestrictionType.CUSTOMERS;
+                case "permit" -> res |= AccessRestrictionType.PERMIT;
                 default -> {
                 }
             }
diff --git a/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilderTest.java b/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilderTest.java
index edd800121e..86c59eb696 100644
--- a/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilderTest.java
+++ b/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilderTest.java
@@ -30,6 +30,43 @@ class RoadAccessRestrictionsGraphStorageBuilderTest {
     public RoadAccessRestrictionsGraphStorageBuilderTest() throws Exception {
     }
 
+    @Test
+    void testAccessTypes() throws Exception {
+        initBuilder(RoutingProfileType.UNKNOWN);
+
+        ReaderWay way = new ReaderWay(1);
+        builder.processWay(way);
+        assertEquals(AccessRestrictionType.NONE, builder.getRestrictions());
+
+        way.setTag("access", "no");
+        builder.processWay(way);
+        assertEquals(AccessRestrictionType.NO, builder.getRestrictions());
+
+        way.setTag("access", "customers");
+        builder.processWay(way);
+        assertEquals(AccessRestrictionType.CUSTOMERS, builder.getRestrictions());
+
+        way.setTag("access", "destination");
+        builder.processWay(way);
+        assertEquals(AccessRestrictionType.DESTINATION, builder.getRestrictions());
+
+        way.setTag("access", "delivery");
+        builder.processWay(way);
+        assertEquals(AccessRestrictionType.DELIVERY, builder.getRestrictions());
+
+        way.setTag("access", "private");
+        builder.processWay(way);
+        assertEquals(AccessRestrictionType.PRIVATE, builder.getRestrictions());
+
+        way.setTag("access", "permissive");
+        builder.processWay(way);
+        assertEquals(AccessRestrictionType.PERMISSIVE, builder.getRestrictions());
+
+        way.setTag("access", "permit");
+        builder.processWay(way);
+        assertEquals(AccessRestrictionType.PERMIT, builder.getRestrictions());
+    }
+
     @Test
     void testCarWayCreation() throws Exception {
         initBuilder(RoutingProfileType.DRIVING_CAR);


Reviews:
user: login: aoles
body: Thanks a lot for you prompt PR ❤️ 

Please have a look at my comments, and address the issues reported by Quality Gate.

Cheers! 
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-25T15:02:36Z
user: login: aoles
body: Many thanks for addressing my comments! 👍 

Please have a look at some additional remarks in the comments below.

The maintainability [issue](https://sonarcloud.io/project/issues?sinceLeakPeriod=true&issueStatuses=OPEN%2CCONFIRMED&pullRequest=2047&id=GIScience_openrouteservice&open=AZZtCKvLTKPQaCOl0Qm-) reported by SonarCloud should be possible to address by changing the general `Exception` thrown to a more specific one such as `IllegalStateException`.

Cheers! ❤️ 
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-29T09:39:21Z
user: login: aoles
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T10:15:25Z
user: login: aoles
body: Thanks for bearing with me! ❤️ 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-29T10:37:22Z

Review comments:
diff_hunk: @@ -29,6 +29,7 @@ Releasing is documented in RELEASE.md
 
 ### Added
 - add hint to use yaml editor in docs ([#1836](https://github.com/GIScience/openrouteservice/issues/1836))
+- Add access = permit to known access restiction types ([#2013](https://github.com/GIScience/openrouteservice/issues/2013))
path: CHANGELOG.md
user: login: aoles
body: Please refrain from starting the entries under *Added* with "add" (similar applies to the category *Fixed* where the entries should avoid repeating repeat "fix"). It's clear from the context that something has been added (or fixed) and the actually interesting part is **what**. Also, please consider formatting tag values as code snippets like `access = permit`. Let's keep the CHANGELOG clean and concise! 💪 
created_at: 2025-04-25T13:24:29Z
author_association: MEMBER
diff_hunk: @@ -47,6 +47,11 @@ void testCarWayCreation() throws Exception {
         way.setTag("motorcar", "yes");
         builder.processWay(way);
         assertEquals(AccessRestrictionType.NONE, builder.getRestrictions());
+
+        way = new ReaderWay(1);
+        way.setTag("access", "permit");
+        builder.processWay(way);
+        assertEquals(AccessRestrictionType.PERMIT, builder.getRestrictions());
path: ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilderTest.java
user: login: aoles
body: I've added a commit that introduces a test intended to cover all values of `AccessRestrictionType` for an unspecified vehicle, so let's leave the original car-specific test `testCarWayCreation` untouched.

The newly introduced test fails, which is expected in the current implementation. To make it work, please adapt the code of `RoadAccessRestrictionsGraphStorageBuilder` to account for `RoutingProfileType.UNKNOWN`. In this process, you might discover some hidden bugs in the current implementation... Have fun! 😉 

https://github.com/GIScience/openrouteservice/blob/78c0612c7d1c8de36185a875e222d1eced1a80b1/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilder.java#L160-L170
created_at: 2025-04-25T15:00:10Z
author_association: MEMBER
diff_hunk: @@ -104,6 +104,10 @@ public static boolean isCycling(int routePref) {
                 || routePref == GH_BIKE_ROAD;
     }
 
+    public static boolean isUNKNOWN(int routePref) {
path: ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfileType.java
user: login: aoles
body: I would personally refrain from introducing this type of trivial predicate method, especially if it's only used once.
created_at: 2025-04-29T09:22:09Z
author_association: MEMBER
diff_hunk: @@ -166,6 +168,8 @@ public void processWay(ReaderWay way, Coordinate[] coords, Map<Integer, Map<Stri
                 restrictions = isAccessAllowed(way, VAL_BICYCLE) ? 0 : getRestrictionType(way, VAL_BICYCLE);
             if (RoutingProfileType.isPedestrian(profileType))
                 restrictions = isAccessAllowed(way, "foot") ? 0 : getRestrictionType(way, "foot");
+            if (RoutingProfileType.isUNKNOWN(profileType))
path: ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilder.java
user: login: aoles
body: Please use a direct check against `profileType == RoutingProfileType.isUNKNOWN` rather than introducing a new method.
created_at: 2025-04-29T09:23:44Z
author_association: MEMBER
diff_hunk: @@ -166,6 +168,8 @@ public void processWay(ReaderWay way, Coordinate[] coords, Map<Integer, Map<Stri
                 restrictions = isAccessAllowed(way, VAL_BICYCLE) ? 0 : getRestrictionType(way, VAL_BICYCLE);
             if (RoutingProfileType.isPedestrian(profileType))
                 restrictions = isAccessAllowed(way, "foot") ? 0 : getRestrictionType(way, "foot");
+            if (RoutingProfileType.isUNKNOWN(profileType))
+                restrictions = isAccessAllowed(way, "unknown") ? 0 : getRestrictionType(way, "unknown");
path: ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilder.java
user: login: aoles
body: The second argument to `isAccessAllowed` and `getRestrictionType` corresponds to OSM access-type key, as described in the [OSM wiki](https://wiki.openstreetmap.org/wiki/Key:access). In its most general form it specifies `access` for all modes of transport, e.g. `access = no` says that nobody may legally access the road. This can be overridden by additional tags defining mode of transport such as `bike = yes`. In this context "unknown" is not a valid value.

What I envisioned was to have a fallback value which does not depend on a specific mode of transport, i.e. which just checks based on the value of the `access` tag. For this, it should be enough to have
```java
restrictions = getRestrictionType(way, "access");
```
created_at: 2025-04-29T09:38:26Z
author_association: MEMBER
diff_hunk: @@ -166,6 +168,8 @@ public void processWay(ReaderWay way, Coordinate[] coords, Map<Integer, Map<Stri
                 restrictions = isAccessAllowed(way, VAL_BICYCLE) ? 0 : getRestrictionType(way, VAL_BICYCLE);
             if (RoutingProfileType.isPedestrian(profileType))
                 restrictions = isAccessAllowed(way, "foot") ? 0 : getRestrictionType(way, "foot");
+            if (profileType == RoutingProfileType.UNKNOWN)
+                restrictions = getRestrictionType(way, "access");;
path: ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/storages/builders/RoadAccessRestrictionsGraphStorageBuilder.java
user: login: aoles
body: You might want to fix the double `;;` 😅 
created_at: 2025-04-29T10:15:25Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-29T10:27:40Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=GIScience_openrouteservice&pullRequest=2047) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=GIScience_openrouteservice&pullRequest=2047&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=GIScience_openrouteservice&pullRequest=2047&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=GIScience_openrouteservice&pullRequest=2047&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [77.8% Coverage on New Code](https://sonarcloud.io/component_measures?id=GIScience_openrouteservice&pullRequest=2047&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=GIScience_openrouteservice&pullRequest=2047&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=GIScience_openrouteservice&pullRequest=2047)


author_association: NONE