docs: public MatchResult doWork(List<Observation> gpxList) {
          // filter the entries:
          List<Observation> filteredGPXEntries = filterGPXEntries(gpxList);
  
          // now find each of the entries in the graph:
          List<Collection<QueryResult>> queriesPerEntry = lookupGPXEntries(filteredGPXEntries, DefaultEdgeFilter.allEdges(weighting.getFlagEncoder()));
  
          // Add virtual nodes and edges to the graph so that candidates on edges can be represented
          // by virtual nodes.
          QueryGraph queryGraph = new QueryGraph(routingGraph).setUseEdgeExplorerCache(true);
          List<QueryResult> allQueryResults = new ArrayList<>();
          for (Collection<QueryResult> qrs : queriesPerEntry) {
              allQueryResults.addAll(qrs);
          }
          queryGraph.lookup(allQueryResults);
  
          // Different QueryResults can have the same tower node as their closest node.
          // Hence, we now dedupe the query results of each GPX entry by their closest node (#91).
          // This must be done after calling queryGraph.lookup() since this replaces some of the
          // QueryResult nodes with virtual nodes. Virtual nodes are not deduped since there is at
          // most one QueryResult per edge and virtual nodes are inserted into the middle of an edge.
          // Reducing the number of QueryResults improves performance since less shortest/fastest
          // routes need to be computed.
          queriesPerEntry = deduplicateQueryResultsByClosestNode(queriesPerEntry);
  
          logger.debug("================= Query results =================");
          int i = 1;
          for (Collection<QueryResult> entries : queriesPerEntry) {
              logger.debug("Query results for GPX entry {}", i++);
              for (QueryResult qr : entries) {
                  logger.debug("Node id: {}, virtual: {}, snapped on: {}, pos: {},{}, "
                                  + "query distance: {}", qr.getClosestNode(),
                          isVirtualNode(qr.getClosestNode()), qr.getSnappedPosition(),
                          qr.getSnappedPoint().getLat(), qr.getSnappedPoint().getLon(),
                          qr.getQueryDistance());
              }
          }
  
          // Creates candidates from the QueryResults of all GPX entries (a candidate is basically a
          // QueryResult + direction).
          List<TimeStep<State, Observation, Path>> timeSteps =
                  createTimeSteps(filteredGPXEntries, queriesPerEntry, queryGraph);
          logger.debug("=============== Time steps ===============");
          i = 1;
          for (TimeStep<State, Observation, Path> ts : timeSteps) {
              logger.debug("Candidates for time step {}", i++);
              for (State candidate : ts.candidates) {
                  logger.debug(candidate.toString());
              }
          }
  
          // Compute the most likely sequence of map matching candidates:
          List<SequenceState<State, Observation, Path>> seq = computeViterbiSequence(timeSteps, gpxList.size(), queryGraph);
  
          logger.debug("=============== Viterbi results =============== ");
          i = 1;
          for (SequenceState<State, Observation, Path> ss : seq) {
              logger.debug("{}: {}, path: {}", i, ss.state,
                      ss.transitionDescriptor != null ? ss.transitionDescriptor.calcEdges() : null);
              i++;
          }
  
          final EdgeExplorer explorer = queryGraph.createEdgeExplorer(DefaultEdgeFilter.allEdges(weighting.getFlagEncoder()));
          final Map<String, EdgeIteratorState> virtualEdgesMap = createVirtualEdgesMap(queriesPerEntry, explorer);
          MatchResult matchResult = computeMatchResult(seq, virtualEdgesMap, gpxList, queryGraph);
          logger.debug("=============== Matched real edges =============== ");
          i = 1;
          for (EdgeMatch em : matchResult.getEdgeMatches()) {
              logger.debug("{}: {}", i, em.getEdgeState());
              i++;
          }
          return matchResult;
      }