Repository: graphhopper/graphhopper
Pull request #2768 (2023-03-07 16:13:44)

Add path details for the time, distance and weight of the single legs
When we query `/route` with via-points it returns the time, distance, weight and coordinates of the entire route. But so far there is no direct information about the time/distance/weight of the single sections between the (via) points. Also it is not very clear how the global point list can be separated for the single sections ('legs'). We can work around this using instructions or path details like 'time', but this is rather cumbersome and enabling instructions or certain path details just for this purpose can decrease performance. 

Therefore, I added three new path details, e.g. with a single via-point we get two entries (because there are two legs) for each detail:

```json
{
	"details": {
		"leg_time": [
			[
				0,
				6,
				48559
			],
			[
				6,
				10,
				60719
			]
		],
		"leg_distance": [
			[
				0,
				6,
				501.46548797983564
			],
			[
				6,
				10,
				505.98093195638285
			]
		],
		"leg_weight": [
			[
				0,
				6,
				48.55733718679518
			],
			[
				6,
				10,
				60.717711834765936
			]
		],
	}
}
```

`leg_time/distance/weight` simply represent the time/distance/weight for the corresponding leg. 

Also note that the first/last indices are equal for all details. They can be used to obtain the coordinates for a certain leg. In this example the points 0-6 represent the leg between the start and the via-point, and points 6-10 represent the leg between the via-point and the destination point.


Patch:
diff --git a/README.md b/README.md
index 5a42db6ec..7c5f3567d 100644
--- a/README.md
+++ b/README.md
@@ -22,14 +22,17 @@ We even have [good first issues](https://github.com/graphhopper/graphhopper/issu
 
 To get started you can try [GraphHopper Maps](README.md#graphhopper-maps), read through our documentation and install the GraphHopper Web Service locally.
 
-* 6.x: [documentation](https://github.com/graphhopper/graphhopper/blob/6.x/docs/index.md)
-  , [web service jar](https://repo1.maven.org/maven2/com/graphhopper/graphhopper-web/6.2/graphhopper-web-6.2.jar)
-  , [announcement](https://www.graphhopper.com/blog/2022/09/19/graphhopper-routing-engine-6-0-released/)
+* 7.x: [documentation](https://github.com/graphhopper/graphhopper/blob/7.x/docs/index.md)
+  , [web service jar](https://repo1.maven.org/maven2/com/graphhopper/graphhopper-web/7.0/graphhopper-web-7.0.jar)
+  , [announcement](https://www.graphhopper.com/blog/2023/03/14/graphhopper-routing-engine-7-0-released/)
 * unstable master: [documentation](https://github.com/graphhopper/graphhopper/blob/master/docs/index.md)
 
 <details><summary>Click to see older releases</summary>
 
 * See our [changelog file](./CHANGELOG.md) for Java API Changes.
+* 6.x: [documentation](https://github.com/graphhopper/graphhopper/blob/6.x/docs/index.md)
+  , [web service jar](https://repo1.maven.org/maven2/com/graphhopper/graphhopper-web/6.2/graphhopper-web-6.2.jar)
+  , [announcement](https://www.graphhopper.com/blog/2022/09/19/graphhopper-routing-engine-6-0-released/)
 * 5.x: [documentation](https://github.com/graphhopper/graphhopper/blob/5.x/docs/index.md)
   , [web service jar](https://github.com/graphhopper/graphhopper/releases/download/5.3/graphhopper-web-5.3.jar)
   , [announcement](https://www.graphhopper.com/blog/2022/03/23/graphhopper-routing-engine-5-0-released/)
@@ -81,7 +84,7 @@ To get started you can try [GraphHopper Maps](README.md#graphhopper-maps), read
 To install the [GraphHopper Maps](https://graphhopper.com/maps/) UI and the web service locally you [need a JVM](https://adoptium.net) (>= Java 8) and do:
 
 ```bash
-wget https://repo1.maven.org/maven2/com/graphhopper/graphhopper-web/6.2/graphhopper-web-6.2.jar https://raw.githubusercontent.com/graphhopper/graphhopper/6.x/config-example.yml http://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf
+wget https://repo1.maven.org/maven2/com/graphhopper/graphhopper-web/7.0/graphhopper-web-7.0.jar https://raw.githubusercontent.com/graphhopper/graphhopper/7.x/config-example.yml http://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf
 java -D"dw.graphhopper.datareader.file=berlin-latest.osm.pbf" -jar graphhopper*.jar server config-example.yml
 ```
 
diff --git a/client-hc/pom.xml b/client-hc/pom.xml
index dd31382f7..425832d61 100644
--- a/client-hc/pom.xml
+++ b/client-hc/pom.xml
@@ -22,14 +22,14 @@
     <modelVersion>4.0.0</modelVersion>
 
     <artifactId>directions-api-client-hc</artifactId>
-    <version>7.0-SNAPSHOT</version>
+    <version>8.0-SNAPSHOT</version>
     <packaging>jar</packaging>
     <name>GraphHopper Directions API hand-crafted Java Client.</name>
      
     <parent>
         <groupId>com.graphhopper</groupId>
         <artifactId>graphhopper-parent</artifactId>
-        <version>7.0-SNAPSHOT</version>
+        <version>8.0-SNAPSHOT</version>
     </parent>  
     
     <dependencies>
diff --git a/core/pom.xml b/core/pom.xml
index d060a9e3b..6d7d227a8 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -5,7 +5,7 @@
 
     <artifactId>graphhopper-core</artifactId>
     <name>GraphHopper Core</name>
-    <version>7.0-SNAPSHOT</version>
+    <version>8.0-SNAPSHOT</version>
     <packaging>jar</packaging>
     <description>
         GraphHopper is a fast and memory efficient Java road routing engine
@@ -14,7 +14,7 @@
     <parent>
         <groupId>com.graphhopper</groupId>
         <artifactId>graphhopper-parent</artifactId>
-        <version>7.0-SNAPSHOT</version>
+        <version>8.0-SNAPSHOT</version>
     </parent>
 
     <properties>
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.
diff --git a/core/src/main/java/com/graphhopper/util/details/ConstantDetailsBuilder.java b/core/src/main/java/com/graphhopper/util/details/ConstantDetailsBuilder.java
new file mode 100644
index 000000000..d308e22ac
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/util/details/ConstantDetailsBuilder.java
@@ -0,0 +1,48 @@
+/*
+ *  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.util.details;
+
+import com.graphhopper.util.EdgeIteratorState;
+
+/**
+ * Simply returns the same value everywhere, useful to represent values that are the same between two (via-)points
+ */
+public class ConstantDetailsBuilder extends AbstractPathDetailsBuilder {
+    private final Object value;
+    private boolean firstEdge = true;
+
+    public ConstantDetailsBuilder(String name, Object value) {
+        super(name);
+        this.value = value;
+    }
+
+    @Override
+    protected Object getCurrentValue() {
+        return value;
+    }
+
+    @Override
+    public boolean isEdgeDifferentToLastEdge(EdgeIteratorState edge) {
+        if (firstEdge) {
+            firstEdge = false;
+            return true;
+        } else
+            return false;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java b/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
index 3719ff129..6e456a728 100644
--- a/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
+++ b/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
@@ -17,6 +17,7 @@
  */
 package com.graphhopper.util.details;
 
+import com.graphhopper.routing.Path;
 import com.graphhopper.routing.ev.*;
 import com.graphhopper.routing.weighting.Weighting;
 import com.graphhopper.storage.Graph;
@@ -33,9 +34,16 @@ import static com.graphhopper.util.Parameters.Details.*;
  */
 public class PathDetailsBuilderFactory {
 
-    public List<PathDetailsBuilder> createPathDetailsBuilders(List<String> requestedPathDetails, EncodedValueLookup evl, Weighting weighting, Graph graph) {
+    public List<PathDetailsBuilder> createPathDetailsBuilders(List<String> requestedPathDetails, Path path, EncodedValueLookup evl, Weighting weighting, Graph graph) {
         List<PathDetailsBuilder> builders = new ArrayList<>();
 
+        if (requestedPathDetails.contains(LEG_TIME))
+            builders.add(new ConstantDetailsBuilder(LEG_TIME, path.getTime()));
+        if (requestedPathDetails.contains(LEG_DISTANCE))
+            builders.add(new ConstantDetailsBuilder(LEG_DISTANCE, path.getDistance()));
+        if (requestedPathDetails.contains(LEG_WEIGHT))
+            builders.add(new ConstantDetailsBuilder(LEG_WEIGHT, path.getWeight()));
+
         if (requestedPathDetails.contains(STREET_NAME))
             builders.add(new KVStringDetails(STREET_NAME));
         if (requestedPathDetails.contains(STREET_REF))
diff --git a/core/src/main/java/com/graphhopper/util/details/PathDetailsFromEdges.java b/core/src/main/java/com/graphhopper/util/details/PathDetailsFromEdges.java
index 3a28ded82..39da194f6 100644
--- a/core/src/main/java/com/graphhopper/util/details/PathDetailsFromEdges.java
+++ b/core/src/main/java/com/graphhopper/util/details/PathDetailsFromEdges.java
@@ -52,8 +52,6 @@ public class PathDetailsFromEdges implements Path.EdgeVisitor {
     /**
      * Calculates the PathDetails for a Path. This method will return fast, if there are no calculators.
      *
-     * @param path
-     * @param weighting
      * @param pathBuilderFactory Generates the relevant PathBuilders
      * @return List of PathDetails for this Path
      */
@@ -62,7 +60,7 @@ public class PathDetailsFromEdges implements Path.EdgeVisitor {
                                                             int previousIndex, Graph graph) {
         if (!path.isFound() || requestedPathDetails.isEmpty())
             return Collections.emptyMap();
-        List<PathDetailsBuilder> pathBuilders = pathBuilderFactory.createPathDetailsBuilders(requestedPathDetails, evLookup, weighting, graph);
+        List<PathDetailsBuilder> pathBuilders = pathBuilderFactory.createPathDetailsBuilders(requestedPathDetails, path, evLookup, weighting, graph);
         if (pathBuilders.isEmpty())
             return Collections.emptyMap();
 
diff --git a/example/pom.xml b/example/pom.xml
index bc9b9cbe0..50e79f133 100644
--- a/example/pom.xml
+++ b/example/pom.xml
@@ -5,14 +5,14 @@
     <modelVersion>4.0.0</modelVersion>
 
     <artifactId>graphhopper-example</artifactId>
-    <version>7.0-SNAPSHOT</version>
+    <version>8.0-SNAPSHOT</version>
     <packaging>jar</packaging>
     <name>GraphHopper Example</name>
 
     <parent>
         <groupId>com.graphhopper</groupId>
         <artifactId>graphhopper-parent</artifactId>
-        <version>7.0-SNAPSHOT</version>
+        <version>8.0-SNAPSHOT</version>
     </parent>  
 
     <dependencies>
diff --git a/map-matching/pom.xml b/map-matching/pom.xml
index 2d6bcb0f3..975e04f6f 100644
--- a/map-matching/pom.xml
+++ b/map-matching/pom.xml
@@ -3,14 +3,14 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.graphhopper</groupId>
     <artifactId>graphhopper-map-matching</artifactId>
-    <version>7.0-SNAPSHOT</version>
+    <version>8.0-SNAPSHOT</version>
     <packaging>jar</packaging>
     <name>GraphHopper Map Matching</name>
 
     <parent>
         <groupId>com.graphhopper</groupId>
         <artifactId>graphhopper-parent</artifactId>
-        <version>7.0-SNAPSHOT</version>
+        <version>8.0-SNAPSHOT</version>
     </parent>
     
     <dependencies>
diff --git a/navigation/pom.xml b/navigation/pom.xml
index a55db2fd6..92ff6dd39 100644
--- a/navigation/pom.xml
+++ b/navigation/pom.xml
@@ -5,14 +5,14 @@
     <modelVersion>4.0.0</modelVersion>
 
     <artifactId>graphhopper-nav</artifactId>
-    <version>7.0-SNAPSHOT</version>
+    <version>8.0-SNAPSHOT</version>
     <packaging>jar</packaging>
     <name>GraphHopper Navigation</name>
 
     <parent>
         <groupId>com.graphhopper</groupId>
         <artifactId>graphhopper-parent</artifactId>
-        <version>7.0-SNAPSHOT</version>
+        <version>8.0-SNAPSHOT</version>
     </parent>  
 
     <dependencies>
diff --git a/pom.xml b/pom.xml
index 2023a097e..71571981b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
     <groupId>com.graphhopper</groupId>
     <artifactId>graphhopper-parent</artifactId>
     <name>GraphHopper Parent Project</name>
-    <version>7.0-SNAPSHOT</version>
+    <version>8.0-SNAPSHOT</version>
     <packaging>pom</packaging>
     <url>https://www.graphhopper.com</url>
     <inceptionYear>2012</inceptionYear>
diff --git a/reader-gtfs/pom.xml b/reader-gtfs/pom.xml
index a5ede60be..9c199152d 100644
--- a/reader-gtfs/pom.xml
+++ b/reader-gtfs/pom.xml
@@ -10,7 +10,7 @@
     <parent>
         <groupId>com.graphhopper</groupId>
         <artifactId>graphhopper-parent</artifactId>
-        <version>7.0-SNAPSHOT</version>
+        <version>8.0-SNAPSHOT</version>
     </parent>
 
     <dependencies>
diff --git a/tools/pom.xml b/tools/pom.xml
index 2aa29aece..c21c1455d 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
     <parent>
         <groupId>com.graphhopper</groupId>
         <artifactId>graphhopper-parent</artifactId>
-        <version>7.0-SNAPSHOT</version>
+        <version>8.0-SNAPSHOT</version>
     </parent>
     <properties>
         <assembly-phase>package</assembly-phase>
diff --git a/web-api/pom.xml b/web-api/pom.xml
index 17f8d81ae..7d55855c2 100644
--- a/web-api/pom.xml
+++ b/web-api/pom.xml
@@ -5,14 +5,14 @@
     <modelVersion>4.0.0</modelVersion>
     <artifactId>graphhopper-web-api</artifactId>
     <packaging>jar</packaging>
-    <version>7.0-SNAPSHOT</version>
+    <version>8.0-SNAPSHOT</version>
     <name>GraphHopper Web API</name>
     <description>JSON Representation of the API classes</description>
 
     <parent>
         <groupId>com.graphhopper</groupId>
         <artifactId>graphhopper-parent</artifactId>
-        <version>7.0-SNAPSHOT</version>
+        <version>8.0-SNAPSHOT</version>
     </parent>
 
     <dependencies>
diff --git a/web-api/src/main/java/com/graphhopper/util/Parameters.java b/web-api/src/main/java/com/graphhopper/util/Parameters.java
index a4e28ba54..1d8e6b658 100644
--- a/web-api/src/main/java/com/graphhopper/util/Parameters.java
+++ b/web-api/src/main/java/com/graphhopper/util/Parameters.java
@@ -205,6 +205,10 @@ public class Parameters {
         public static final String WEIGHT = "weight";
         public static final String DISTANCE = "distance";
         public static final String INTERSECTION = "intersection";
+
+        public static final String LEG_TIME = "leg_time";
+        public static final String LEG_DISTANCE = "leg_distance";
+        public static final String LEG_WEIGHT = "leg_weight";
     }
 
 }
diff --git a/web-bundle/pom.xml b/web-bundle/pom.xml
index 7d7c791e1..853496a4c 100644
--- a/web-bundle/pom.xml
+++ b/web-bundle/pom.xml
@@ -5,7 +5,7 @@
     <modelVersion>4.0.0</modelVersion>
     <artifactId>graphhopper-web-bundle</artifactId>
     <packaging>jar</packaging>
-    <version>7.0-SNAPSHOT</version>
+    <version>8.0-SNAPSHOT</version>
     <properties>
         <graphhopper-maps.version>0.0.0-8922383db6f937fddb44acc702f2b45844566575</graphhopper-maps.version>
     </properties>
@@ -15,7 +15,7 @@
     <parent>
         <groupId>com.graphhopper</groupId>
         <artifactId>graphhopper-parent</artifactId>
-        <version>7.0-SNAPSHOT</version>
+        <version>8.0-SNAPSHOT</version>
     </parent>
 
     <dependencies>
diff --git a/web/pom.xml b/web/pom.xml
index 62e2c7b2b..765dce7b0 100644
--- a/web/pom.xml
+++ b/web/pom.xml
@@ -5,14 +5,14 @@
     <modelVersion>4.0.0</modelVersion>
     <artifactId>graphhopper-web</artifactId>
     <packaging>jar</packaging>
-    <version>7.0-SNAPSHOT</version>
+    <version>8.0-SNAPSHOT</version>
     <name>GraphHopper Web</name>
     <description>Use the GraphHopper routing engine as a web-service</description>
 
     <parent>
         <groupId>com.graphhopper</groupId>
         <artifactId>graphhopper-parent</artifactId>
-        <version>7.0-SNAPSHOT</version>
+        <version>8.0-SNAPSHOT</version>
     </parent>
     <properties>
         <shade-phase>package</shade-phase>


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/RouteResourceClientHCTest.java b/web/src/test/java/com/graphhopper/application/resources/RouteResourceClientHCTest.java
index ff6510ecc..8fb7e43e5 100644
--- a/web/src/test/java/com/graphhopper/application/resources/RouteResourceClientHCTest.java
+++ b/web/src/test/java/com/graphhopper/application/resources/RouteResourceClientHCTest.java
@@ -46,6 +46,8 @@ import org.junit.jupiter.params.provider.EnumSource;
 import java.io.File;
 import java.util.*;
 
+import static com.graphhopper.util.Instruction.FINISH;
+import static com.graphhopper.util.Instruction.REACHED_VIA;
 import static org.junit.jupiter.api.Assertions.*;
 
 /**
@@ -379,4 +381,86 @@ public class RouteResourceClientHCTest {
         assertFalse(rsp.hasErrors(), "errors:" + rsp.getErrors().toString());
         assertEquals(218_000, rsp.getBest().getTime(), 1000);
     }
+
+    @ParameterizedTest
+    @EnumSource(value = TestParam.class)
+    public void testWaypointIndicesAndLegDetails(TestParam p) {
+        GraphHopperWeb gh = createGH(p);
+        List<String> legDetails = Arrays.asList("leg_time", "leg_distance", "leg_weight");
+        GHRequest req = new GHRequest().
+                addPoint(new GHPoint(42.509141, 1.546063)).
+                addPoint(new GHPoint(42.507173, 1.531902)).
+                addPoint(new GHPoint(42.505435, 1.515943)).
+                addPoint(new GHPoint(42.499062, 1.506067)).
+                addPoint(new GHPoint(42.498801, 1.505568)).
+                addPoint(new GHPoint(42.498465, 1.504898)).
+                addPoint(new GHPoint(42.49833, 1.504619)).
+                addPoint(new GHPoint(42.498217, 1.504377)).
+                addPoint(new GHPoint(42.495611, 1.498368)).
+                setPathDetails(legDetails).
+                setProfile("bike");
+
+        GHResponse response = gh.route(req);
+        ResponsePath path = response.getBest();
+        assertEquals(5333, path.getDistance(), 5);
+        assertEquals(9, path.getWaypoints().size());
+
+        assertEquals(path.getTime(), path.getPathDetails().get("leg_time").stream().mapToLong(d -> (long) d.getValue()).sum(), 1);
+        assertEquals(path.getDistance(), path.getPathDetails().get("leg_distance").stream().mapToDouble(d -> (double) d.getValue()).sum(), 1);
+        assertEquals(path.getRouteWeight(), path.getPathDetails().get("leg_weight").stream().mapToDouble(d -> (double) d.getValue()).sum(), 1);
+
+        List<PointList> pointListFromInstructions = getPointListFromInstructions(path);
+        for (String detail : legDetails) {
+            List<PathDetail> pathDetails = path.getPathDetails().get(detail);
+
+            // explicitly check one of the waypoints
+            assertEquals(42.50539, path.getWaypoints().get(2).lat);
+            assertEquals(42.50539, path.getPoints().get(pathDetails.get(1).getLast()).getLat());
+            assertEquals(42.50539, path.getPoints().get(pathDetails.get(2).getFirst()).getLat());
+            // check all the waypoints
+            assertEquals(path.getWaypoints().get(0), path.getPoints().get(pathDetails.get(0).getFirst()));
+            for (int i = 1; i < path.getWaypoints().size(); ++i)
+                assertEquals(path.getWaypoints().get(i), path.getPoints().get(pathDetails.get(i - 1).getLast()));
+
+            List<PointList> pointListFromLegDetails = getPointListFromLegDetails(path, detail);
+            assertEquals(8, pointListFromLegDetails.size());
+            assertPointListsEquals(pointListFromInstructions, pointListFromLegDetails);
+        }
+    }
+
+    private List<PointList> getPointListFromInstructions(ResponsePath path) {
+        List<PointList> legs = new ArrayList<>();
+        PointList perLeg = new PointList();
+        for (Instruction instruction : path.getInstructions()) {
+            perLeg.add(instruction.getPoints());
+            if (instruction.getSign() == REACHED_VIA || instruction.getSign() == FINISH) {
+                legs.add(perLeg);
+                perLeg = new PointList();
+            } else {
+                perLeg.removeLastPoint();
+            }
+        }
+        return legs;
+    }
+
+    private List<PointList> getPointListFromLegDetails(ResponsePath path, String detail) {
+        List<PointList> legs = new ArrayList<>();
+        List<PathDetail> legDetails = path.getPathDetails().get(detail);
+        for (PathDetail legDetail : legDetails) {
+            PointList leg = new PointList(legDetail.getLast() - legDetail.getFirst() + 1, path.getPoints().is3D());
+            for (int j = legDetail.getFirst(); j <= legDetail.getLast(); j++)
+                leg.add(path.getPoints(), j);
+            legs.add(leg);
+        }
+        return legs;
+    }
+
+    private static void assertPointListsEquals(List<PointList> p, List<PointList> q) {
+        assertEquals(p.size(), q.size());
+        for (int i = 0; i < q.size(); i++) {
+            assertEquals(p.get(i).size(), q.get(i).size());
+            for (int j = 0; j < q.get(i).size(); j++)
+                assertEquals(p.get(i).get(j), q.get(i).get(j));
+        }
+    }
 }
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);
diff --git a/web/src/test/java/com/graphhopper/application/resources/RouteResourceTest.java b/web/src/test/java/com/graphhopper/application/resources/RouteResourceTest.java
index eaed06572..d364a777b 100644
--- a/web/src/test/java/com/graphhopper/application/resources/RouteResourceTest.java
+++ b/web/src/test/java/com/graphhopper/application/resources/RouteResourceTest.java
@@ -54,6 +54,8 @@ import java.util.*;
 
 import static com.graphhopper.application.util.TestUtils.clientTarget;
 import static com.graphhopper.application.util.TestUtils.clientUrl;
+import static com.graphhopper.util.Instruction.FINISH;
+import static com.graphhopper.util.Instruction.REACHED_VIA;
 import static com.graphhopper.util.Parameters.NON_CH.MAX_NON_CH_POINT_DISTANCE;
 import static org.junit.jupiter.api.Assertions.*;
 
@@ -623,4 +625,86 @@ public class RouteResourceTest {
         JsonNode json = response.readEntity(JsonNode.class);
         assertEquals("The number of 'heading' parameters must be zero, one or equal to the number of points (1)", json.get("message").asText());
     }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    void legDetailsAndPointIndices(boolean instructions) {
+        final long seed = 123L;
+        Random rnd = new Random(seed);
+        List<String> legDetails = Arrays.asList("leg_time", "leg_distance", "leg_weight");
+        int errors = 0;
+        for (int numPoints = 2; numPoints < 10; numPoints++) {
+            String url = "/route?profile=my_car&points_encoded=false&instructions=" + instructions;
+            for (int i = 0; i < numPoints; i++) {
+                double lat = 42.493748 + rnd.nextDouble() * (42.565155 - 42.493748);
+                double lon = 1.487522 + rnd.nextDouble() * (1.557285 - 1.487522);
+                url += "&point=" + lat + "," + lon;
+            }
+            for (String legDetail : legDetails)
+                url += "&details=" + legDetail;
+            final Response response = clientTarget(app, url).request().buildGet().invoke();
+            JsonNode json = response.readEntity(JsonNode.class);
+            if (response.getStatus() != 200) {
+                // sometimes there can be connection-not-found for example, also because we set min_network_size to 0 in this test
+                errors++;
+                continue;
+            }
+            assertFalse(json.has("message"));
+            JsonNode path = json.get("paths").get(0);
+            JsonNode points = path.get("points");
+            JsonNode snappedWaypoints = path.get("snapped_waypoints");
+            assertEquals(numPoints, snappedWaypoints.get("coordinates").size());
+
+            assertEquals(path.get("time").asDouble(), sumDetail(path.get("details").get("leg_time")), 1);
+            assertEquals(path.get("distance").asDouble(), sumDetail(path.get("details").get("leg_distance")), 1);
+            assertEquals(path.get("weight").asDouble(), sumDetail(path.get("details").get("leg_weight")), 1);
+
+            for (String detail : legDetails) {
+                JsonNode legDetail = path.get("details").get(detail);
+                assertEquals(numPoints - 1, legDetail.size());
+                assertEquals(snappedWaypoints.get("coordinates").get(0), points.get("coordinates").get(legDetail.get(0).get(0).asInt()));
+                for (int i = 1; i < numPoints; i++)
+                    // we make sure that the intervals defined by the leg details start/end at the snapped waypoints
+                    assertEquals(snappedWaypoints.get("coordinates").get(i), points.get("coordinates").get(legDetail.get(i - 1).get(1).asInt()));
+
+                if (instructions) {
+                    // we can find the way point indices also from the instructions, so we check if this yields the same
+                    List<Integer> waypointIndicesFromInstructions = getWaypointIndicesFromInstructions(path.get("instructions"));
+                    List<Integer> waypointIndicesFromLegDetails = getWaypointIndicesFromLegDetails(legDetail);
+                    assertEquals(waypointIndicesFromInstructions, waypointIndicesFromLegDetails);
+                }
+            }
+        }
+        if (errors > 3)
+            fail("too many errors");
+    }
+
+    private static List<Integer> getWaypointIndicesFromInstructions(JsonNode instructions) {
+        List<Integer> result = new ArrayList<>();
+        result.add(instructions.get(0).get("interval").get(0).asInt());
+        for (int i = 0; i < instructions.size(); i++) {
+            int sign = instructions.get(i).get("sign").asInt();
+            if (sign == REACHED_VIA || sign == FINISH)
+                result.add(instructions.get(i).get("interval").get(1).asInt());
+        }
+        return result;
+    }
+
+    private static List<Integer> getWaypointIndicesFromLegDetails(JsonNode detail) {
+        List<Integer> result = new ArrayList<>();
+        result.add(detail.get(0).get(0).asInt());
+        for (int i = 0; i < detail.size(); i++) {
+            if (i > 0)
+                assertEquals(detail.get(i - 1).get(1).asInt(), detail.get(i).get(0).asInt());
+            result.add(detail.get(i).get(1).asInt());
+        }
+        return result;
+    }
+
+    private double sumDetail(JsonNode detail) {
+        double result = 0;
+        for (int i = 0; i < detail.size(); i++)
+            result += detail.get(i).get(2).asDouble();
+        return result;
+    }
 }