docs: public CHEntry runSearch(int targetNode, int targetEdge) {
          // if source and target are equal we already have a candidate for the best path: a simple turn from the source
          // to the target edge
          bestPathWeight = sourceNode == targetNode
                  ? calcTurnWeight(sourceEdge, sourceNode, targetEdge)
                  : Double.POSITIVE_INFINITY;
          bestPathIncEdge = NO_EDGE;
          bestPathIsBridgePath = false;
  
          // check if we can already reach the target from the shortest path tree we discovered so far
          EdgeIterator inIter = origInEdgeExplorer.setBaseNode(targetNode);
          while (inIter.next()) {
              final int incEdge = inIter.getOrigEdgeLast();
              final int edgeKey = getEdgeKey(incEdge, targetNode);
              if (edges[edgeKey] != NO_EDGE) {
                  boolean isZeroWeightLoop = parents[edgeKey] >= 0 && targetNode == adjNodes[parents[edgeKey]] &&
                          weights[edgeKey] - weights[parents[edgeKey]] <= MAX_ZERO_WEIGHT_LOOP;
                  if (!isZeroWeightLoop) {
                      // we may not update the best path if we are dealing with a zero weight loop here, because when a
                      // zero weight loop updates the best path to be no longer a bridge path we cannot trust that there
                      // will be a shortcut leading to the zero weight loop in case there are multiple zero weight loops.
                      updateBestPath(targetNode, targetEdge, edgeKey);
                  }
              }
          }
  
          // run dijkstra to find the optimal path
          while (!dijkstraHeap.isEmpty()) {
              if (numPathsToCenter < 1 && (!bestPathIsBridgePath || isInfinite(bestPathWeight))) {
                  // we have not found a connection to the target edge yet and there are no entries on the heap anymore 
                  // that could yield a bridge-path
                  break;
              }
              final int currKey = dijkstraHeap.peek_element();
              if (weights[currKey] > bestPathWeight) {
                  // just reaching this edge is more expensive than the best path found so far including the turn costs
                  // to reach the target edge -> we can stop
                  // important: we only peeked so far, so we keep the entry for future searches
                  break;
              }
              dijkstraHeap.poll_element();
              numPolledEdges++;
              currentBatchStats.numPolledEdges++;
              totalStats.numPolledEdges++;
  
              if (isPathToCenters[currKey]) {
                  numPathsToCenter--;
              }
  
              // after a certain amount of edges has been settled we only expand entries that might yield a bridge-path
              if (numSettledEdges > maxSettledEdges && !isPathToCenters[currKey]) {
                  continue;
              }
  
              final int fromNode = adjNodes[currKey];
              EdgeIterator iter = outEdgeExplorer.setBaseNode(fromNode);
              while (iter.next()) {
                  if (isContracted(iter.getAdjNode())) {
                      continue;
                  }
                  // do not allow u-turns
                  if (iter.getOrigEdgeFirst() == incEdges[currKey]) {
                      continue;
                  }
                  double edgeWeight = turnWeighting.calcWeight(iter, false, incEdges[currKey]);
                  double weight = edgeWeight + weights[currKey];
                  if (isInfinite(weight)) {
                      continue;
                  }
                  boolean isPathToCenter = this.isPathToCenters[currKey] && iter.getAdjNode() == centerNode;
                  boolean isZeroWeightLoop = fromNode == targetNode && edgeWeight <= MAX_ZERO_WEIGHT_LOOP;
  
                  // dijkstra expansion: add or update current entries
                  int key = getEdgeKey(iter.getOrigEdgeLast(), iter.getAdjNode());
                  if (edges[key] == NO_EDGE) {
                      setEntry(key, iter, weight, currKey, isPathToCenter);
                      changedEdges.add(key);
                      dijkstraHeap.insert_(weight, key);
                      if (!isZeroWeightLoop) {
                          updateBestPath(targetNode, targetEdge, key);
                      }
                  } else if (weight < weights[key]) {
                      updateEntry(key, iter, weight, currKey, isPathToCenter);
                      dijkstraHeap.update_(weight, key);
                      if (!isZeroWeightLoop) {
                          updateBestPath(targetNode, targetEdge, key);
                      }
                  }
              }
              numSettledEdges++;
              currentBatchStats.numSettledEdges++;
              totalStats.numSettledEdges++;
              // do not keep searching after target node has been expanded first time, should speed up contraction a bit but
              // leads to less witnesses being found.
  //            if (adjNodes[currKey] == targetNode) {
  //                break;
  //            }
          }
  
          if (bestPathIsBridgePath) {
              int edgeKey = getEdgeKey(bestPathIncEdge, targetNode);
              CHEntry result = getEntryForKey(edgeKey);
              // prepend all ancestors
              CHEntry entry = result;
              while (parents[edgeKey] >= 0) {
                  edgeKey = parents[edgeKey];
                  CHEntry parent = getEntryForKey(edgeKey);
                  entry.parent = parent;
                  entry = parent;
              }
              entry.parent = initialEntryParents.get(parents[edgeKey]);
              return result;
          } else {
              return null;
          }
      }