Repository: graphhopper/graphhopper
Pull request #2767 (2023-03-07 12:36:24)

Show u-turn instruction for plain u-turns
Currently there are no u-turn instructions for 'simple' u-turns where we simply turn around on the same road/edge. We only show instructions for such u-turns when the occur at via-points, and we even show instructions for the (more complicated) u-turns that use a via-way:

![image](https://user-images.githubusercontent.com/17603532/223421733-0fcdf672-b445-42df-b146-41b08ff9a86c.png)

The image shows a u-turn at the via-point 1, blue marker. We get an instruction for this, but not the u-turn at the end of the dead-end road near the red marker. The reason we get this u-turn is a curbside constraint at the red marker.

Here is an example for a u-turn that includes a via-way, and for which we already show a u-turn instruction:

![image](https://user-images.githubusercontent.com/17603532/223422569-aa7079f7-889d-4d26-9684-ae43424bc873.png)

In this PR I added a u-turn instruction for the simple kind of u-turns, like the one near the red marker in the first screenshot.

I also wonder if we should distinguish between these simple (but often dangerous) u-turns and the ones with via-ways, that might be perfectly safe (or completely forbidden when there is a no-u-turn sign...).

Related: #2139 where a user suggested that the sharp/simple/dangerous u-turns should be avoided as much as possible anyway. I think I agree, but at the very least we should make these turns more visible, hence this PR. For example, a navigation application could inform the driver that the route leads to a u-turn and instead of having to turn around all of a sudden the driver could start looking for a safe option to turn around.


Patch:
diff --git a/core/src/main/java/com/graphhopper/reader/ReaderElement.java b/core/src/main/java/com/graphhopper/reader/ReaderElement.java
index 3784f0811..9007af9cb 100644
--- a/core/src/main/java/com/graphhopper/reader/ReaderElement.java
+++ b/core/src/main/java/com/graphhopper/reader/ReaderElement.java
@@ -74,10 +74,10 @@ public abstract class ReaderElement {
         return properties;
     }
 
-    public void setTags(Map<String, String> newTags) {
+    public void setTags(Map<String, Object> newTags) {
         properties.clear();
         if (newTags != null)
-            for (Entry<String, String> e : newTags.entrySet()) {
+            for (Entry<String, Object> e : newTags.entrySet()) {
                 setTag(e.getKey(), e.getValue());
             }
     }
diff --git a/core/src/main/java/com/graphhopper/reader/osm/OSMInputFile.java b/core/src/main/java/com/graphhopper/reader/osm/OSMInputFile.java
index 296d39f07..1741ee5cd 100644
--- a/core/src/main/java/com/graphhopper/reader/osm/OSMInputFile.java
+++ b/core/src/main/java/com/graphhopper/reader/osm/OSMInputFile.java
@@ -55,6 +55,7 @@ public class OSMInputFile implements Sink, OSMInput {
     private Thread pbfReaderThread;
     private boolean hasIncomingData;
     private int workerThreads = -1;
+    private SkipOptions skipOptions = SkipOptions.none();
     private OSMFileHeader fileheader;
 
     public OSMInputFile(File file) throws IOException {
@@ -73,13 +74,22 @@ public class OSMInputFile implements Sink, OSMInput {
     }
 
     /**
-     * Currently on for pbf format. Default is number of cores.
+     * Currently only for pbf format. Default is number of cores.
      */
     public OSMInputFile setWorkerThreads(int threads) {
         workerThreads = threads;
         return this;
     }
 
+    /**
+     * Use this to prevent the creation of OSM nodes, ways and/or relations to speed up the file reading process.
+     * This will only affect the reading of pbf files.
+     */
+    public OSMInputFile setSkipOptions(SkipOptions skipOptions) {
+        this.skipOptions = skipOptions;
+        return this;
+    }
+
     @SuppressWarnings("unchecked")
     private InputStream decode(File file) throws IOException {
         final String name = file.getName();
@@ -247,7 +257,7 @@ public class OSMInputFile implements Sink, OSMInput {
         if (workerThreads <= 0)
             workerThreads = 1;
 
-        pbfReader = new PbfReader(stream, this, workerThreads);
+        pbfReader = new PbfReader(stream, this, workerThreads, skipOptions);
         pbfReaderThread = new Thread(pbfReader, "PBF Reader");
         pbfReaderThread.start();
     }
diff --git a/core/src/main/java/com/graphhopper/reader/osm/SkipOptions.java b/core/src/main/java/com/graphhopper/reader/osm/SkipOptions.java
new file mode 100644
index 000000000..a4b25a72b
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/reader/osm/SkipOptions.java
@@ -0,0 +1,47 @@
+/*
+ *  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.reader.osm;
+
+public class SkipOptions {
+    private final boolean skipNodes;
+    private final boolean skipWays;
+    private final boolean skipRelations;
+
+    public static SkipOptions none() {
+        return new SkipOptions(false, false, false);
+    }
+
+    public SkipOptions(boolean skipNodes, boolean skipWays, boolean skipRelations) {
+        this.skipNodes = skipNodes;
+        this.skipWays = skipWays;
+        this.skipRelations = skipRelations;
+    }
+
+    public boolean isSkipNodes() {
+        return skipNodes;
+    }
+
+    public boolean isSkipWays() {
+        return skipWays;
+    }
+
+    public boolean isSkipRelations() {
+        return skipRelations;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/reader/osm/WaySegmentParser.java b/core/src/main/java/com/graphhopper/reader/osm/WaySegmentParser.java
index c6c2319d4..89dba9820 100644
--- a/core/src/main/java/com/graphhopper/reader/osm/WaySegmentParser.java
+++ b/core/src/main/java/com/graphhopper/reader/osm/WaySegmentParser.java
@@ -107,7 +107,7 @@ public class WaySegmentParser {
         LOGGER.info("Start reading OSM file: '" + osmFile + "'");
         LOGGER.info("pass1 - start");
         StopWatch sw1 = StopWatch.started();
-        readOSM(osmFile, new Pass1Handler());
+        readOSM(osmFile, new Pass1Handler(), new SkipOptions(true, false, false));
         LOGGER.info("pass1 - finished, took: {}", sw1.stop().getTimeString());
 
         long nodes = nodeData.getNodeCount();
@@ -116,7 +116,7 @@ public class WaySegmentParser {
 
         LOGGER.info("pass2 - start");
         StopWatch sw2 = new StopWatch().start();
-        readOSM(osmFile, new Pass2Handler());
+        readOSM(osmFile, new Pass2Handler(), SkipOptions.none());
         LOGGER.info("pass2 - finished, took: {}", sw2.stop().getTimeString());
 
         nodeData.release();
@@ -389,8 +389,8 @@ public class WaySegmentParser {
         }
     }
 
-    private void readOSM(File file, ReaderElementHandler handler) {
-        try (OSMInput osmInput = openOsmInputFile(file)) {
+    private void readOSM(File file, ReaderElementHandler handler, SkipOptions skipOptions) {
+        try (OSMInput osmInput = openOsmInputFile(file, skipOptions)) {
             ReaderElement elem;
             while ((elem = osmInput.getNext()) != null)
                 handler.handleElement(elem);
@@ -402,8 +402,8 @@ public class WaySegmentParser {
         }
     }
 
-    protected OSMInput openOsmInputFile(File osmFile) throws XMLStreamException, IOException {
-        return new OSMInputFile(osmFile).setWorkerThreads(workerThreads).open();
+    protected OSMInput openOsmInputFile(File osmFile, SkipOptions skipOptions) throws XMLStreamException, IOException {
+        return new OSMInputFile(osmFile).setWorkerThreads(workerThreads).setSkipOptions(skipOptions).open();
     }
 
     public static class Builder {
diff --git a/core/src/main/java/com/graphhopper/reader/osm/pbf/PbfBlobDecoder.java b/core/src/main/java/com/graphhopper/reader/osm/pbf/PbfBlobDecoder.java
index 70c154f8d..13b41b5b2 100644
--- a/core/src/main/java/com/graphhopper/reader/osm/pbf/PbfBlobDecoder.java
+++ b/core/src/main/java/com/graphhopper/reader/osm/pbf/PbfBlobDecoder.java
@@ -8,6 +8,7 @@ import com.graphhopper.reader.ReaderNode;
 import com.graphhopper.reader.ReaderRelation;
 import com.graphhopper.reader.ReaderWay;
 import com.graphhopper.reader.osm.OSMFileHeader;
+import com.graphhopper.reader.osm.SkipOptions;
 import com.graphhopper.util.Helper;
 import org.openstreetmap.osmosis.osmbinary.Fileformat;
 import org.openstreetmap.osmosis.osmbinary.Osmformat;
@@ -33,6 +34,7 @@ public class PbfBlobDecoder implements Runnable {
     private final byte[] rawBlob;
     private final PbfBlobDecoderListener listener;
     private List<ReaderElement> decodedEntities;
+    private final SkipOptions skipOptions;
 
     /**
      * Creates a new instance.
@@ -42,10 +44,11 @@ public class PbfBlobDecoder implements Runnable {
      * @param rawBlob  The raw data of the blob.
      * @param listener The listener for receiving decoding results.
      */
-    public PbfBlobDecoder(String blobType, byte[] rawBlob, PbfBlobDecoderListener listener) {
+    public PbfBlobDecoder(String blobType, byte[] rawBlob, PbfBlobDecoderListener listener, SkipOptions skipOptions) {
         this.blobType = blobType;
         this.rawBlob = rawBlob;
         this.listener = listener;
+        this.skipOptions = skipOptions;
     }
 
     private byte[] readBlobContent() throws IOException {
@@ -116,7 +119,7 @@ public class PbfBlobDecoder implements Runnable {
          */
     }
 
-    private Map<String, String> buildTags(List<Integer> keys, List<Integer> values, PbfFieldDecoder fieldDecoder) {
+    private Map<String, Object> buildTags(List<Integer> keys, List<Integer> values, PbfFieldDecoder fieldDecoder) {
 
         // Ensure parallel lists are of equal size.
         if (checkData) {
@@ -129,7 +132,7 @@ public class PbfBlobDecoder implements Runnable {
         Iterator<Integer> keyIterator = keys.iterator();
         Iterator<Integer> valueIterator = values.iterator();
         if (keyIterator.hasNext()) {
-            Map<String, String> tags = new HashMap<>(keys.size());
+            Map<String, Object> tags = new HashMap<>(keys.size());
             while (keyIterator.hasNext()) {
                 String key = fieldDecoder.decodeString(keyIterator.next());
                 String value = fieldDecoder.decodeString(valueIterator.next());
@@ -142,7 +145,7 @@ public class PbfBlobDecoder implements Runnable {
 
     private void processNodes(List<Osmformat.Node> nodes, PbfFieldDecoder fieldDecoder) {
         for (Osmformat.Node node : nodes) {
-            Map<String, String> tags = buildTags(node.getKeysList(), node.getValsList(), fieldDecoder);
+            Map<String, Object> tags = buildTags(node.getKeysList(), node.getValsList(), fieldDecoder);
 
             ReaderNode osmNode = new ReaderNode(node.getId(), fieldDecoder.decodeLatitude(node
                     .getLat()), fieldDecoder.decodeLatitude(node.getLon()));
@@ -215,7 +218,7 @@ public class PbfBlobDecoder implements Runnable {
             // Build the tags. The key and value string indexes are sequential
             // in the same PBF array. Each set of tags is delimited by an index
             // with a value of 0.
-            Map<String, String> tags = null;
+            Map<String, Object> tags = null;
             while (keysValuesIterator.hasNext()) {
                 int keyIndex = keysValuesIterator.next();
                 if (keyIndex == 0) {
@@ -247,7 +250,7 @@ public class PbfBlobDecoder implements Runnable {
 
     private void processWays(List<Osmformat.Way> ways, PbfFieldDecoder fieldDecoder) {
         for (Osmformat.Way way : ways) {
-            Map<String, String> tags = buildTags(way.getKeysList(), way.getValsList(), fieldDecoder);
+            Map<String, Object> tags = buildTags(way.getKeysList(), way.getValsList(), fieldDecoder);
             ReaderWay osmWay = new ReaderWay(way.getId());
             osmWay.setTags(tags);
 
@@ -308,7 +311,7 @@ public class PbfBlobDecoder implements Runnable {
 
     private void processRelations(List<Osmformat.Relation> relations, PbfFieldDecoder fieldDecoder) {
         for (Osmformat.Relation relation : relations) {
-            Map<String, String> tags = buildTags(relation.getKeysList(), relation.getValsList(), fieldDecoder);
+            Map<String, Object> tags = buildTags(relation.getKeysList(), relation.getValsList(), fieldDecoder);
 
             ReaderRelation osmRelation = new ReaderRelation(relation.getId());
             osmRelation.setTags(tags);
@@ -326,10 +329,14 @@ public class PbfBlobDecoder implements Runnable {
         PbfFieldDecoder fieldDecoder = new PbfFieldDecoder(block);
 
         for (Osmformat.PrimitiveGroup primitiveGroup : block.getPrimitivegroupList()) {
-            processNodes(primitiveGroup.getDense(), fieldDecoder);
-            processNodes(primitiveGroup.getNodesList(), fieldDecoder);
-            processWays(primitiveGroup.getWaysList(), fieldDecoder);
-            processRelations(primitiveGroup.getRelationsList(), fieldDecoder);
+            if (!skipOptions.isSkipNodes()) {
+                processNodes(primitiveGroup.getDense(), fieldDecoder);
+                processNodes(primitiveGroup.getNodesList(), fieldDecoder);
+            }
+            if (!skipOptions.isSkipWays())
+                processWays(primitiveGroup.getWaysList(), fieldDecoder);
+            if (!skipOptions.isSkipRelations())
+                processRelations(primitiveGroup.getRelationsList(), fieldDecoder);
         }
     }
 
diff --git a/core/src/main/java/com/graphhopper/reader/osm/pbf/PbfDecoder.java b/core/src/main/java/com/graphhopper/reader/osm/pbf/PbfDecoder.java
index 89763ac54..396c0c505 100644
--- a/core/src/main/java/com/graphhopper/reader/osm/pbf/PbfDecoder.java
+++ b/core/src/main/java/com/graphhopper/reader/osm/pbf/PbfDecoder.java
@@ -2,6 +2,7 @@
 package com.graphhopper.reader.osm.pbf;
 
 import com.graphhopper.reader.ReaderElement;
+import com.graphhopper.reader.osm.SkipOptions;
 
 import java.util.LinkedList;
 import java.util.List;
@@ -26,6 +27,7 @@ public class PbfDecoder {
     private final Lock lock;
     private final Condition dataWaitCondition;
     private final Queue<PbfBlobResult> blobResults;
+    private final SkipOptions skipOptions;
 
     /**
      * Creates a new instance.
@@ -37,11 +39,12 @@ public class PbfDecoder {
      * @param sink            The sink to send all decoded entities to.
      */
     public PbfDecoder(PbfStreamSplitter streamSplitter, ExecutorService executorService, int maxPendingBlobs,
-                      Sink sink) {
+                      Sink sink, SkipOptions skipOptions) {
         this.streamSplitter = streamSplitter;
         this.executorService = executorService;
         this.maxPendingBlobs = maxPendingBlobs;
         this.sink = sink;
+        this.skipOptions = skipOptions;
 
         // Create the thread synchronisation primitives.
         lock = new ReentrantLock();
@@ -140,7 +143,7 @@ public class PbfDecoder {
             };
 
             // Create the blob decoder itself and execute it on a worker thread.
-            PbfBlobDecoder blobDecoder = new PbfBlobDecoder(rawBlob.getType(), rawBlob.getData(), decoderListener);
+            PbfBlobDecoder blobDecoder = new PbfBlobDecoder(rawBlob.getType(), rawBlob.getData(), decoderListener, skipOptions);
             executorService.execute(blobDecoder);
 
             // If the number of pending blobs has reached capacity we must begin
diff --git a/core/src/main/java/com/graphhopper/reader/osm/pbf/PbfReader.java b/core/src/main/java/com/graphhopper/reader/osm/pbf/PbfReader.java
index 8e7a20177..02313f5f2 100644
--- a/core/src/main/java/com/graphhopper/reader/osm/pbf/PbfReader.java
+++ b/core/src/main/java/com/graphhopper/reader/osm/pbf/PbfReader.java
@@ -1,6 +1,8 @@
 // This software is released into the Public Domain.  See copying.txt for details.
 package com.graphhopper.reader.osm.pbf;
 
+import com.graphhopper.reader.osm.SkipOptions;
+
 import java.io.DataInputStream;
 import java.io.InputStream;
 import java.util.concurrent.ExecutorService;
@@ -14,9 +16,10 @@ import java.util.concurrent.Executors;
  */
 public class PbfReader implements Runnable {
     private Throwable throwable;
-    private InputStream inputStream;
-    private Sink sink;
-    private int workers;
+    private final InputStream inputStream;
+    private final Sink sink;
+    private final int workers;
+    private final SkipOptions skipOptions;
 
     /**
      * Creates a new instance.
@@ -25,10 +28,11 @@ public class PbfReader implements Runnable {
      * @param in      The file to read.
      * @param workers The number of worker threads for decoding PBF blocks.
      */
-    public PbfReader(InputStream in, Sink sink, int workers) {
+    public PbfReader(InputStream in, Sink sink, int workers, SkipOptions skipOptions) {
         this.inputStream = in;
         this.sink = sink;
         this.workers = workers;
+        this.skipOptions = skipOptions;
     }
 
     @Override
@@ -44,7 +48,7 @@ public class PbfReader implements Runnable {
             // immediately ready for processing when a worker thread completes.
             // The main thread is responsible for splitting blobs from the
             // request stream, and sending decoded entities to the sink.
-            PbfDecoder pbfDecoder = new PbfDecoder(streamSplitter, executorService, workers + 1, sink);
+            PbfDecoder pbfDecoder = new PbfDecoder(streamSplitter, executorService, workers + 1, sink, skipOptions);
             pbfDecoder.run();
 
         } catch (Throwable t) {
diff --git a/core/src/main/java/com/graphhopper/routing/InstructionsFromEdges.java b/core/src/main/java/com/graphhopper/routing/InstructionsFromEdges.java
index e8cce5509..a25b8fae4 100644
--- a/core/src/main/java/com/graphhopper/routing/InstructionsFromEdges.java
+++ b/core/src/main/java/com/graphhopper/routing/InstructionsFromEdges.java
@@ -332,6 +332,9 @@ public class InstructionsFromEdges implements Path.EdgeVisitor {
     }
 
     private int getTurn(EdgeIteratorState edge, int baseNode, int prevNode, int adjNode, String name, String destinationAndRef) {
+        if (edge.getEdge() == prevEdge.getEdge())
+            // this is the simplest turn to recognize, a plain u-turn.
+            return Instruction.U_TURN_UNKNOWN;
         GHPoint point = InstructionsHelper.getPointForOrientationCalculation(edge, nodeAccess);
         double lat = point.getLat();
         double lon = point.getLon();
diff --git a/core/src/main/java/com/graphhopper/routing/querygraph/QueryOverlayBuilder.java b/core/src/main/java/com/graphhopper/routing/querygraph/QueryOverlayBuilder.java
index c44857c90..e05b58ef4 100644
--- a/core/src/main/java/com/graphhopper/routing/querygraph/QueryOverlayBuilder.java
+++ b/core/src/main/java/com/graphhopper/routing/querygraph/QueryOverlayBuilder.java
@@ -33,6 +33,8 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 
+import static com.graphhopper.util.DistancePlaneProjection.DIST_PLANE;
+
 class QueryOverlayBuilder {
     private final int firstVirtualNodeId;
     private final int firstVirtualEdgeId;
@@ -157,7 +159,8 @@ class QueryOverlayBuilder {
                 // Create base and adjacent PointLists for all non-equal virtual nodes.
                 // We do so via inserting them at the correct position of fullPL and cutting the
                 // fullPL into the right pieces.
-                for (Snap res : results) {
+                for (int i = 0; i < results.size(); i++) {
+                    Snap res = results.get(i);
                     if (res.getClosestEdge().getBaseNode() != baseNode)
                         throw new IllegalStateException("Base nodes have to be identical but were not: " + closestEdge + " vs " + res.getClosestEdge());
 
@@ -166,6 +169,10 @@ class QueryOverlayBuilder {
                     // no new virtual nodes if very close ("snap" together)
                     if (Snap.considerEqual(prevPoint.lat, prevPoint.lon, currSnapped.lat, currSnapped.lon)) {
                         res.setClosestNode(prevNodeId);
+                        res.setSnappedPoint(prevPoint);
+                        res.setWayIndex(i == 0 ? 0 : results.get(i - 1).getWayIndex());
+                        res.setSnappedPosition(i == 0 ? Snap.Position.TOWER : results.get(i - 1).getSnappedPosition());
+                        res.setQueryDistance(DIST_PLANE.calcDist(prevPoint.lat, prevPoint.lon, res.getQueryPoint().lat, res.getQueryPoint().lon));
                         continue;
                     }
 
diff --git a/core/src/main/java/com/graphhopper/storage/index/LocationIndexTree.java b/core/src/main/java/com/graphhopper/storage/index/LocationIndexTree.java
index 0a0ca9c50..9243ba7db 100644
--- a/core/src/main/java/com/graphhopper/storage/index/LocationIndexTree.java
+++ b/core/src/main/java/com/graphhopper/storage/index/LocationIndexTree.java
@@ -302,8 +302,8 @@ public class LocationIndexTree implements LocationIndex {
         }
 
         if (closestMatch.isValid()) {
-            closestMatch.setQueryDistance(DIST_PLANE.calcDenormalizedDist(closestMatch.getQueryDistance()));
             closestMatch.calcSnappedPoint(DIST_PLANE);
+            closestMatch.setQueryDistance(DIST_PLANE.calcDist(closestMatch.getSnappedPoint().lat, closestMatch.getSnappedPoint().lon, queryLat, queryLon));
         }
         return closestMatch;
     }
diff --git a/core/src/main/java/com/graphhopper/storage/index/Snap.java b/core/src/main/java/com/graphhopper/storage/index/Snap.java
index e843de564..7ef64be1a 100644
--- a/core/src/main/java/com/graphhopper/storage/index/Snap.java
+++ b/core/src/main/java/com/graphhopper/storage/index/Snap.java
@@ -124,6 +124,10 @@ public class Snap {
         return snappedPoint;
     }
 
+    public void setSnappedPoint(GHPoint3D point) {
+        this.snappedPoint = point;
+    }
+
     /**
      * Calculates the closest point on the edge from the query point. If too close to a tower or pillar node this method
      * might change the snappedPosition and wayIndex.


Test patch:
diff --git a/core/src/test/java/com/graphhopper/GraphHopperTest.java b/core/src/test/java/com/graphhopper/GraphHopperTest.java
index fe3bb9ea8..0e8090fe9 100644
--- a/core/src/test/java/com/graphhopper/GraphHopperTest.java
+++ b/core/src/test/java/com/graphhopper/GraphHopperTest.java
@@ -226,16 +226,41 @@ public class GraphHopperTest {
     }
 
     @Test
-    public void testUTurn() {
+    public void testUTurnInstructions() {
         final String profile = "profile";
         final String vehicle = "car";
         GraphHopper hopper = new GraphHopper().
                 setGraphHopperLocation(GH_LOCATION).
                 setOSMFile(MONACO).
-                setProfiles(new CustomProfile(profile).setCustomModel(new CustomModel()).setVehicle(vehicle).setTurnCosts(true));
+                setProfiles(new CustomProfile(profile).setCustomModel(new CustomModel()).setVehicle(vehicle).setTurnCosts(true).putHint(U_TURN_COSTS, 20));
         hopper.importOrLoad();
         Translation tr = hopper.getTranslationMap().getWithFallBack(Locale.US);
 
+        {
+            GHRequest request = new GHRequest();
+            request.addPoint(new GHPoint(43.747418, 7.430371));
+            request.addPoint(new GHPoint(43.746853, 7.42974));
+            request.addPoint(new GHPoint(43.746929, 7.430458));
+            request.setProfile(profile);
+            request.setCurbsides(Arrays.asList("right", "any", "right"));
+            GHResponse rsp = hopper.route(request);
+            assertFalse(rsp.hasErrors(), rsp.getErrors().toString());
+            ResponsePath res = rsp.getBest();
+            assertEquals(286, res.getDistance(), 1);
+            // note that this includes the u-turn time for the second u-turn, but not the first, because it's a waypoint!
+            assertEquals(54351, res.getTime(), 1);
+            // the route follows Avenue de l'Annonciade to the waypoint, u-turns there, then does a sharp right turn onto the parallel (dead-end) road,
+            // does a u-turn at the dead-end and then arrives at the destination
+            InstructionList il = res.getInstructions();
+            assertEquals(6, il.size());
+            assertEquals("continue", il.get(0).getTurnDescription(tr));
+            assertEquals("waypoint 1", il.get(1).getTurnDescription(tr));
+            assertEquals("make a U-turn", il.get(2).getTurnDescription(tr));
+            assertEquals("turn sharp right", il.get(3).getTurnDescription(tr));
+            assertEquals("make a U-turn", il.get(4).getTurnDescription(tr));
+            assertEquals("arrive at destination", il.get(5).getTurnDescription(tr));
+        }
+
         {
             GHRequest request = new GHRequest();
             request.addPoint(new GHPoint(43.743887, 7.431151));
diff --git a/core/src/test/java/com/graphhopper/reader/osm/ReaderElementTest.java b/core/src/test/java/com/graphhopper/reader/osm/ReaderElementTest.java
index 4589b7fc8..6c2413c2a 100644
--- a/core/src/test/java/com/graphhopper/reader/osm/ReaderElementTest.java
+++ b/core/src/test/java/com/graphhopper/reader/osm/ReaderElementTest.java
@@ -41,7 +41,7 @@ public class ReaderElementTest {
     @Test
     public void testSetTags() {
         ReaderElement instance = new ReaderWay(1);
-        Map<String, String> map = new HashMap<>();
+        Map<String, Object> map = new HashMap<>();
         map.put("test", "xy");
         instance.setTags(map);
         assertTrue(instance.hasTag("test", "xy"));
diff --git a/web/src/test/java/com/graphhopper/application/resources/RouteResourceLeipzigTest.java b/web/src/test/java/com/graphhopper/application/resources/RouteResourceLeipzigTest.java
index 28d07e045..cd31b5423 100644
--- a/web/src/test/java/com/graphhopper/application/resources/RouteResourceLeipzigTest.java
+++ b/web/src/test/java/com/graphhopper/application/resources/RouteResourceLeipzigTest.java
@@ -29,7 +29,6 @@ import io.dropwizard.testing.junit5.DropwizardAppExtension;
 import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.RepeatedTest;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
@@ -68,14 +67,15 @@ public class RouteResourceLeipzigTest {
     @Test
     void testNoErrors() {
         // we just send a bunch of routing requests to make sure there are no internal server errors
-        double minLat = 51.319685;
-        double maxLat = 51.367294;
-        double minLon = 12.335525;
-        double maxLon = 12.434745;
-        final long seed = System.nanoTime();
+        queryRandomRoutes(100, 51.319685, 51.367294, 12.335525, 12.434745);
+        // repeat the same for a very small bounding box to better cover cases where the query points are close together
+        queryRandomRoutes(1000, 51.342534, 51.34285, 12.384917, 12.385278);
+    }
 
+    private static void queryRandomRoutes(int numQueries, double minLat, double maxLat, double minLon, double maxLon) {
+        final long seed = System.nanoTime();
         Random rnd = new Random(seed);
-        for (int i = 0; i < 100; i++) {
+        for (int i = 0; i < numQueries; i++) {
             double latFrom = minLat + rnd.nextDouble() * (maxLat - minLat);
             double lonFrom = minLon + rnd.nextDouble() * (maxLon - minLon);
             double latTo = minLat + rnd.nextDouble() * (maxLat - minLat);