Skip to content

v0.2.54..v0.2.55 changeset ChangesetReplacementCreator.cpp

Garret Voltz edited this page Aug 14, 2020 · 1 revision
diff --git a/hoot-core/src/main/cpp/hoot/core/algorithms/changeset/ChangesetReplacementCreator.cpp b/hoot-core/src/main/cpp/hoot/core/algorithms/changeset/ChangesetReplacementCreator.cpp
index 32ca201..c2f23e0 100644
--- a/hoot-core/src/main/cpp/hoot/core/algorithms/changeset/ChangesetReplacementCreator.cpp
+++ b/hoot-core/src/main/cpp/hoot/core/algorithms/changeset/ChangesetReplacementCreator.cpp
@@ -27,12 +27,14 @@
 #include "ChangesetReplacementCreator.h"
 
 // Hoot
-#include <hoot/core/algorithms/alpha-shape/AlphaShapeGenerator.h>
 #include <hoot/core/algorithms/ReplacementSnappedWayJoiner.h>
 #include <hoot/core/algorithms/WayJoinerAdvanced.h>
 #include <hoot/core/algorithms/WayJoinerBasic.h>
 
+#include <hoot/core/algorithms/alpha-shape/AlphaShapeGenerator.h>
+
 #include <hoot/core/conflate/CookieCutter.h>
+#include <hoot/core/conflate/SuperfluousConflateOpRemover.h>
 #include <hoot/core/conflate/UnifyingConflator.h>
 #include <hoot/core/conflate/network/NetworkMatchCreator.h>
 
@@ -44,45 +46,59 @@
 #include <hoot/core/criterion/OrCriterion.h>
 #include <hoot/core/criterion/PointCriterion.h>
 #include <hoot/core/criterion/PolygonCriterion.h>
+#include <hoot/core/criterion/RelationWithLinearMembersCriterion.h>
+#include <hoot/core/criterion/RelationWithPointMembersCriterion.h>
+#include <hoot/core/criterion/RelationWithPolygonMembersCriterion.h>
+#include <hoot/core/criterion/RoundaboutCriterion.h>
 #include <hoot/core/criterion/TagCriterion.h>
 #include <hoot/core/criterion/TagKeyCriterion.h>
 #include <hoot/core/criterion/WayNodeCriterion.h>
 
-#include <hoot/core/elements/OsmUtils.h>
+#include <hoot/core/elements/ElementDeduplicator.h>
+#include <hoot/core/elements/MapUtils.h>
+
+#include <hoot/core/index/OsmMapIndex.h>
+
+#include <hoot/core/io/IoUtils.h>
 #include <hoot/core/io/OsmGeoJsonReader.h>
 #include <hoot/core/io/OsmMapReaderFactory.h>
 #include <hoot/core/io/OsmMapWriterFactory.h>
 
 #include <hoot/core/ops/ElementIdToVersionMapper.h>
-#include <hoot/core/ops/UnconnectedWaySnapper.h>
-
+#include <hoot/core/ops/MapCleaner.h>
 #include <hoot/core/ops/MapCropper.h>
 #include <hoot/core/ops/NamedOp.h>
 #include <hoot/core/ops/PointsToPolysConverter.h>
 #include <hoot/core/ops/SuperfluousNodeRemover.h>
 #include <hoot/core/ops/SuperfluousWayRemover.h>
+#include <hoot/core/ops/RecursiveElementRemover.h>
 #include <hoot/core/ops/RecursiveSetTagValueOp.h>
 #include <hoot/core/ops/RemoveEmptyRelationsOp.h>
+#include <hoot/core/ops/UnconnectedWaySnapper.h>
 #include <hoot/core/ops/WayJoinerOp.h>
 
 #include <hoot/core/util/Boundable.h>
+#include <hoot/core/util/CollectionUtils.h>
 #include <hoot/core/util/ConfigOptions.h>
+#include <hoot/core/util/ConfigUtils.h>
 #include <hoot/core/util/Factory.h>
 #include <hoot/core/util/GeometryUtils.h>
-#include <hoot/core/io/IoUtils.h>
 #include <hoot/core/util/MapProjector.h>
+#include <hoot/core/util/MemoryUsageChecker.h>
 
 #include <hoot/core/visitors/ApiTagTruncateVisitor.h>
 #include <hoot/core/visitors/FilteredVisitor.h>
 #include <hoot/core/visitors/RemoveElementsVisitor.h>
+#include <hoot/core/visitors/RemoveDuplicateRelationMembersVisitor.h>
 #include <hoot/core/visitors/RemoveMissingElementsVisitor.h>
+#include <hoot/core/visitors/ReportMissingElementsVisitor.h>
 #include <hoot/core/visitors/SetTagValueVisitor.h>
 
 namespace hoot
 {
 
-ChangesetReplacementCreator::ChangesetReplacementCreator(const bool printStats,
-                                                         const QString osmApiDbUrl) :
+ChangesetReplacementCreator::ChangesetReplacementCreator(
+  const bool printStats, const QString& statsOutputFile, const QString osmApiDbUrl) :
 _fullReplacement(false),
 _lenientBounds(true),
 _geometryFiltersSpecified(false),
@@ -90,9 +106,11 @@ _chainReplacementFilters(false),
 _chainRetainmentFilters(false),
 _waySnappingEnabled(true),
 _conflationEnabled(true),
+_cleaningEnabled(true),
 _tagOobConnectedWays(false)
 {
-  _changesetCreator.reset(new ChangesetCreator(printStats, osmApiDbUrl));
+  _changesetCreator.reset(new ChangesetCreator(printStats, statsOutputFile, osmApiDbUrl));
+
   setGeometryFilters(QStringList());
 }
 
@@ -130,18 +148,18 @@ void ChangesetReplacementCreator::setGeometryFilters(const QStringList& filterCl
       else
       {
         _geometryTypeFilters[filter->getGeometryType()] =
-          std::shared_ptr<OrCriterion>(new OrCriterion(currentFilter, filter));
+          OrCriterionPtr(new OrCriterion(currentFilter, filter));
       }
 
       if (filter->getGeometryType() == GeometryTypeCriterion::GeometryType::Line)
       {
         _linearFilterClassNames.append(filterClassName);
-      }
-    }
+      } 
+    } 
   }
 
-  // TODO: have to call this method to keep filtering from erroring...shouldn't have to...should
-  // init itself internally when no geometry filters are specified
+  // have to call this method to keep filtering from erroring...shouldn't have to...should just init
+  // itself internally when no geometry filters are specified
   LOG_VART(_geometryTypeFilters.size());
   if (_geometryTypeFilters.isEmpty())
   {
@@ -267,6 +285,78 @@ void ChangesetReplacementCreator::setReplacementFilterOptions(const QStringList&
   _setInputFilterOptions(_replacementFilterOptions, optionKvps);
 }
 
+QString ChangesetReplacementCreator::_getJobDescription(
+  const QString& input1, const QString& input2, const QString& bounds,
+  const QString& output) const
+{
+  const int maxFilePrintLength = ConfigOptions().getProgressVarPrintLengthMax();
+  QString lenientStr = "Bounds calculation is ";
+  if (!_lenientBounds)
+  {
+    lenientStr += "not ";
+  }
+  lenientStr += "lenient.";
+  const QString replacementTypeStr = _fullReplacement ? "full" : "overlapping only";
+  QString geometryFiltersStr = "are ";
+  if (!_geometryFiltersSpecified)
+  {
+    geometryFiltersStr += "not ";
+  }
+  geometryFiltersStr += "specified";
+  QString replacementFiltersStr = "is ";
+  if (!_replacementFilter)
+  {
+    replacementFiltersStr += "not ";
+  }
+  replacementFiltersStr += "specified";
+  QString retainmentFiltersStr = "is ";
+  if (!_retainmentFilter)
+  {
+    retainmentFiltersStr += "not ";
+  }
+  retainmentFiltersStr += "specified";
+  QString conflationStr = "is ";
+  if (!_conflationEnabled)
+  {
+    conflationStr += "not ";
+  }
+  conflationStr += "enabled";
+  QString cleaningStr = "is ";
+  if (!_cleaningEnabled)
+  {
+    cleaningStr += "not ";
+  }
+  cleaningStr += "enabled";
+  QString waySnappingStr = "is ";
+  if (!_waySnappingEnabled)
+  {
+    waySnappingStr += "not ";
+  }
+  waySnappingStr += "enabled";
+  QString oobWayHandlingStr = "is ";
+  if (!_tagOobConnectedWays)
+  {
+    oobWayHandlingStr += "not ";
+  }
+  oobWayHandlingStr += "enabled";
+
+  QString str;
+  str += "Deriving replacement output changeset:";
+  str += "\nBeing replaced: ..." + input1.right(maxFilePrintLength);
+  str += "\nReplacing with ..." + input2.right(maxFilePrintLength);
+  str += "\nOutput Changeset: ..." + output.right(maxFilePrintLength);
+  str += "\nBounds: " + bounds + lenientStr;
+  str += "\nReplacement is: " + replacementTypeStr;
+  str += "\nGeometry filters: " + geometryFiltersStr;
+  str += "\nReplacement filter: " + replacementFiltersStr;
+  str += "\nRetainment filter: " + retainmentFiltersStr;
+  str += "\nConflation: " + conflationStr;
+  str += "\nCleaning: " + cleaningStr;
+  str += "\nWay snapping: " + waySnappingStr;
+  str += "\nOut of bounds way handling: " + oobWayHandlingStr;
+  return str;
+}
+
 void ChangesetReplacementCreator::setRetainmentFilterOptions(const QStringList& optionKvps)
 {
   LOG_DEBUG("Creating retainment filter options...");
@@ -277,15 +367,14 @@ void ChangesetReplacementCreator::create(
   const QString& input1, const QString& input2, const geos::geom::Envelope& bounds,
   const QString& output)
 {
-  LOG_VARD(_chainReplacementFilters);
-  LOG_VARD(_lenientBounds);
-  LOG_VARD(_fullReplacement);
-
   // INPUT VALIDATION AND SETUP
 
   _validateInputs(input1, input2);
   const QString boundsStr = GeometryUtils::envelopeToConfigString(bounds);
   _setGlobalOpts(boundsStr);
+
+  LOG_INFO(_getJobDescription(input1, input2, boundsStr, output));
+
   // If a retainment filter was specified, we'll AND it together with each geometry type filter to
   // further restrict what reference data gets replaced in the final changeset.
   const QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr> refFilters =
@@ -295,19 +384,7 @@ void ChangesetReplacementCreator::create(
   const QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr> secFilters =
     _getCombinedFilters(_replacementFilter);
 
-  const int maxFilePrintLength = ConfigOptions().getProgressVarPrintLengthMax();
-  QString lenientStr = "Bounds calculation is ";
-  if (!_lenientBounds)
-  {
-    lenientStr += "not ";
-  }
-  lenientStr += "lenient.";
-  LOG_INFO(
-    "Deriving replacement output changeset: ..." << output.right(maxFilePrintLength) <<
-    " from inputs: ..." << input1.right(maxFilePrintLength) << " and ..." <<
-    input2.right(maxFilePrintLength) << "" << ", at bounds: " << boundsStr << ". " << lenientStr);
-
-  // CHANGESET CALCULATION
+  // DIFF CALCULATION
 
   // Since data with different geometry types require different settings, we'll calculate a separate
   // pair of before/after maps for each geometry type.
@@ -318,23 +395,30 @@ void ChangesetReplacementCreator::create(
   for (QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>::const_iterator itr =
          refFilters.begin(); itr != refFilters.end(); ++itr)
   {
-    LOG_VARD("******************************************");
+    LOG_INFO("******************************************");
     LOG_INFO(
       "Preparing maps for changeset derivation given geometry type: "<<
       GeometryTypeCriterion::typeToString(itr.key()) << ". Pass: " << passCtr << " / " <<
       refFilters.size() << "...");
 
     OsmMapPtr refMap;
+    // This is a bit of a misnomer after recent changes, as this map may have only been cleaned by
+    // this point and not actually conflated with anything.
     OsmMapPtr conflatedMap;
     QStringList linearFilterClassNames;
-    //LOG_VARD(itr.value().get());
     if (itr.key() == GeometryTypeCriterion::GeometryType::Line)
     {
       linearFilterClassNames = _linearFilterClassNames;
     }
+
+    ElementCriterionPtr refFilter = itr.value();
+    LOG_VARD(refFilter->toString());
+    ElementCriterionPtr secFilter = secFilters[itr.key()];
+    LOG_VARD(secFilter->toString());
+
     _getMapsForGeometryType(
-      refMap, conflatedMap, input1, input2, boundsStr, itr.value(), secFilters[itr.key()],
-      itr.key(), linearFilterClassNames);
+      refMap, conflatedMap, input1, input2, boundsStr, refFilter, secFilter, itr.key(),
+      linearFilterClassNames);
 
     if (!refMap)
     {
@@ -372,19 +456,43 @@ void ChangesetReplacementCreator::create(
   LOG_VART(conflatedMaps.size());
   if (refMaps.size() == 0 && conflatedMaps.size() == 0)
   {
-    LOG_WARN("No features remain after filtering so no changeset will be generated.");
+    LOG_WARN("No features remain after filtering, so no changeset will be generated.");
     return;
   }
-  assert(refMaps.size() == conflatedMaps.size());
+  if (refMaps.size() != conflatedMaps.size())
+  {
+    throw HootException("Replacement changeset derivation internal map count mismatch error.");
+  }
+
+  // CLEANUP
+
+  // Due to the mixed relations processing explained in _getDefaultGeometryFilters, we may have
+  // some duplicated features that need to be cleaned up before we generate the changesets. This
+  // is kind of a band-aid :-(
+
+  // If we have the maps for only one geometry type, then there isn't a possibility of duplication
+  // created by the replacement operation.
+  if (ConfigOptions().getChangesetReplacementDeduplicateCalculatedMaps() && refMaps.size() > 1)
+  {
+    // Not completely sure at this point if we need to dedupe ref maps. Doing so breaks the
+    // roundabouts test and adds an extra relation to the out of spec test when we do intra-map
+    // de-duping. Mostly worried that not doing so could break the overlapping only replacement
+    // (non-full) scenario...we'll see...
+    //_dedupeMaps(refMaps);
+    _dedupeMaps(conflatedMaps);
+  }
 
   // CHANGESET GENERATION
 
   // Derive a changeset between the ref and conflated maps that replaces ref features with
   // secondary features within the bounds and write it out.
-
+  _changesetCreator->setIncludeReviews(
+    _conflationEnabled && ConfigOptions().getChangesetReplacementPassConflateReviews());
   _changesetCreator->create(refMaps, conflatedMaps, output);
 
-  LOG_INFO("Derived replacement changeset: ..." << output.right(maxFilePrintLength));
+  LOG_STATUS(
+    "Derived replacement changeset: ..." <<
+    output.right(ConfigOptions().getProgressVarPrintLengthMax()));
 }
 
 void ChangesetReplacementCreator::_getMapsForGeometryType(
@@ -403,14 +511,25 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
 
   // DATA LOAD AND INITIAL PREP
 
-  // Load the ref dataset and crop to the specified aoi.
-
+  // load the ref dataset and crop to the specified aoi
   refMap = _loadRefMap(input1);
+  MemoryUsageChecker::getInstance().check();
+
+  const bool markMissing =
+    ConfigOptions().getChangesetReplacementMarkElementsWithMissingChildren();
+  if (markMissing)
+  {
+    // Find any elements with missing children and tag them with a custom tag, as its possible we'll
+    // break them during this process. There's really nothing that can be done about that, since we
+    // don't have access to the missing children elements. Any elements with missing children may
+    // require manual cleanup after the resulting changeset is applied.
+    _markElementsWithMissingChildren(refMap);
+  }
 
   // Keep a mapping of the original ref element ids to versions, as we'll need the original
   // versions later.
-
   const QMap<ElementId, long> refIdToVersionMappings = _getIdToVersionMappings(refMap);
+
   const bool isLinearCrit = !linearFilterClassNames.isEmpty();
   LOG_VART(isLinearCrit);
   if (_tagOobConnectedWays && _lenientBounds && isLinearCrit)
@@ -422,18 +541,21 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
 
   // Prune the ref dataset down to just the geometry types specified by the filter, so we don't end
   // up modifying anything else.
-
   _filterFeatures(
     refMap, refFeatureFilter, conf(),
     "ref-after-" + GeometryTypeCriterion::typeToString(geometryType) + "-pruning");
 
-  // Load the sec dataset and crop to the specified aoi.
-
+  // load the sec dataset and crop to the specified aoi
   OsmMapPtr secMap = _loadSecMap(input2);
+  MemoryUsageChecker::getInstance().check();
+
+  if (markMissing)
+  {
+    _markElementsWithMissingChildren(secMap);
+  }
 
   // Prune the sec dataset down to just the feature types specified by the filter, so we don't end
   // up modifying anything else.
-
   _filterFeatures(
     secMap, secFeatureFilter, _replacementFilterOptions,
     "sec-after-" + GeometryTypeCriterion::typeToString(geometryType) + "-pruning");
@@ -445,10 +567,9 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
   LOG_VARD(refMapSize);
   LOG_VARD(secMapSize);
 
-  // COOKIE CUT
-
-  // Cut the secondary data out of the reference data.
+  // CUT
 
+  // cut the secondary data out of the reference data
   OsmMapPtr cookieCutRefMap = _getCookieCutMap(refMap, secMap, geometryType);
 
   // At one point it was necessary to re-number the relations in the sec map, as they could have ID
@@ -460,30 +581,46 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
 
   // Combine the cookie cut ref map back with the secondary map, so we can conflate the two
   // together.
-
   _combineMaps(cookieCutRefMap, secMap, false, "combined-before-conflation");
   secMap.reset();
 
-  // CONFLATE
+  // CONFLATE / CLEAN
 
-  // Conflate the cookie cut ref map with the sec map.
+  // conflate the cookie cut ref map with the sec map if conflation is enabled
 
   conflatedMap = cookieCutRefMap;
-  if (_conflationEnabled && secMapSize > 0)
+  if (secMapSize > 0)
   {
-    // TODO: do something with reviews - #3361
-    _conflate(conflatedMap, _lenientBounds);
+    if (_conflationEnabled)
+    {
+      // conflation cleans beforehand
+      _conflate(conflatedMap, _lenientBounds);
+      conflatedMap->setName("conflated");
+
+      if (!ConfigOptions().getChangesetReplacementPassConflateReviews())
+      {
+        // remove all conflate reviews
+        _removeConflateReviews(conflatedMap);
+      }
+    }
+    // This is a little misleading to only clean when the sec map has elements, however a test fails
+    // if we don't. May need further investigation.
+    else if (_cleaningEnabled)
+    {
+      _clean(conflatedMap);
+      conflatedMap->setName("cleaned");
+    }
   }
-  conflatedMap->setName("conflated");
+
+  // SNAP
 
   if (isLinearCrit && _waySnappingEnabled)
   {
     // Snap secondary features back to reference features if dealing with linear features where
     // ref features may have been cut along the bounds. We're being lenient here by snapping
-    // secondary to reference *and* allowing conflated data to be snapped to either dataset.
-
-    // We only want to snap ways of like types together, so we'll loop through each applicable
-    // linear type and snap them separately.
+    // secondary to reference *and* allowing conflated data to be snapped to either dataset. We only
+    // want to snap ways of like types together, so we'll loop through each applicable linear type
+    // and snap them separately.
 
     QStringList snapWayStatuses("Input2");
     snapWayStatuses.append("Conflated");
@@ -498,7 +635,6 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
 
     // After snapping, perform joining to prevent unnecessary create/delete statements for the ref
     // data in the resulting changeset and generate modify statements instead.
-
     ReplacementSnappedWayJoiner(refIdToVersionMappings).join(conflatedMap);
     LOG_VART(MapProjector::toWkt(conflatedMap->getProjection()));
   }
@@ -517,16 +653,16 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
   }
 
   // Crop the original ref and conflated maps appropriately for changeset derivation.
-
   const geos::geom::Envelope bounds = GeometryUtils::envelopeFromConfigString(boundsStr);
   _cropMapForChangesetDerivation(
     refMap, bounds, _boundsOpts.changesetRefKeepEntireCrossingBounds,
-    _boundsOpts.changesetRefKeepOnlyInsideBounds, isLinearCrit, "ref-cropped-for-changeset");
+    _boundsOpts.changesetRefKeepOnlyInsideBounds, "ref-cropped-for-changeset");
   _cropMapForChangesetDerivation(
     conflatedMap, bounds, _boundsOpts.changesetSecKeepEntireCrossingBounds,
-    _boundsOpts.changesetSecKeepOnlyInsideBounds, isLinearCrit, "sec-cropped-for-changeset");
+    _boundsOpts.changesetSecKeepOnlyInsideBounds, "sec-cropped-for-changeset");
   LOG_VART(_lenientBounds);
   LOG_VART(isLinearCrit);
+
   if (_lenientBounds && isLinearCrit)
   {
     if (_waySnappingEnabled)
@@ -553,14 +689,12 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
       }
     }
 
-    // Combine the conflated map with the immediately connected out of bounds ways.
-
+    // combine the conflated map with the immediately connected out of bounds ways
     _combineMaps(
       conflatedMap, immediatelyConnectedOutOfBoundsWays, true, "conflated-connected-combined");
 
     // Snap only the connected ways to other ways in the conflated map. Mark the ways that were
     // snapped, as we'll need that info in the next step.
-
     if (_waySnappingEnabled)
     {
       LOG_VART(linearFilterClassNames);
@@ -572,13 +706,11 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
       }
     }
 
-    // Remove any ways that weren't snapped.
-
+    // remove any ways that weren't snapped
     _removeUnsnappedImmediatelyConnectedOutOfBoundsWays(conflatedMap);
 
     // Copy the connected ways back into the ref map as well, so the changeset will derive
     // properly.
-
     _combineMaps(refMap, immediatelyConnectedOutOfBoundsWays, true, "ref-connected-combined");
 
     immediatelyConnectedOutOfBoundsWays.reset();
@@ -588,35 +720,17 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
     // If we're not allowing the changeset deriver to generate delete statements for reference
     // features outside of the bounds, we need to mark all corresponding ref ways with a custom
     // tag that will cause the deriver to skip deleting them.
-
     _excludeFeaturesFromChangesetDeletion(refMap, boundsStr);
   }
 
-  _cleanupMissingElements(refMap);
-  _cleanupMissingElements(conflatedMap);
+  // clean up introduced mistakes
+  _cleanup(refMap);
+  _cleanup(conflatedMap);
 
   LOG_VART(refMap->getElementCount());
   LOG_VART(conflatedMap->getElementCount());
 }
 
-void ChangesetReplacementCreator::_cleanupMissingElements(OsmMapPtr& map)
-{
-  //SuperfluousWayRemover::removeWays(map);
-  //SuperfluousNodeRemover::removeNodes(map);
-
-  // This will handle removing refs in relation members we've cropped out.
-  RemoveMissingElementsVisitor missingElementsRemover;
-  LOG_STATUS("\t" << missingElementsRemover.getInitStatusMessage());
-  map->visitRw(missingElementsRemover);
-  LOG_STATUS("\t" << missingElementsRemover.getCompletedStatusMessage());
-
-  // This will remove any relations that were already empty or became empty after the previous step.
-  RemoveEmptyRelationsOp emptyRelationRemover;
-  LOG_STATUS("\t" << emptyRelationRemover.getInitStatusMessage());
-  emptyRelationRemover.apply(map);
-  LOG_STATUS("\t" << emptyRelationRemover.getCompletedStatusMessage());
-}
-
 void ChangesetReplacementCreator::_validateInputs(const QString& input1, const QString& input2)
 {
   // Fail if the reader that supports either input doesn't implement Boundable.
@@ -658,59 +772,361 @@ void ChangesetReplacementCreator::_validateInputs(const QString& input1, const Q
   }
 }
 
-QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>
-  ChangesetReplacementCreator::_getDefaultGeometryFilters() const
+void ChangesetReplacementCreator::_setGlobalOpts(const QString& boundsStr)
 {
-  QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr> featureFilters;
-  featureFilters[GeometryTypeCriterion::GeometryType::Point] =
-    std::shared_ptr<ElementCriterion>(new PointCriterion());
-  featureFilters[GeometryTypeCriterion::GeometryType::Line] =
-    std::shared_ptr<ElementCriterion>(new LinearCriterion());
-  featureFilters[GeometryTypeCriterion::GeometryType::Polygon] =
-    std::shared_ptr<ElementCriterion>(new PolygonCriterion());
-  return featureFilters;
+  conf().set(ConfigOptions::getChangesetXmlWriterAddTimestampKey(), false);
+  conf().set(ConfigOptions::getReaderAddSourceDatetimeKey(), false);
+  conf().set(ConfigOptions::getWriterIncludeCircularErrorTagsKey(), false);
+  conf().set(ConfigOptions::getConvertBoundingBoxKey(), boundsStr);
+
+  // For this being enabled to have any effect,
+  // convert.bounding.box.keep.immediately.connected.ways.outside.bounds must be enabled as well.
+  conf().set(
+    ConfigOptions::getConvertBoundingBoxTagImmediatelyConnectedOutOfBoundsWaysKey(),
+    _tagOobConnectedWays);
+
+  // will have to see if setting this to false causes problems in the future...
+  conf().set(ConfigOptions::getConvertRequireAreaForPolygonKey(), false);
+
+  // This needs to be lowered a bit from the default of 7 to make feature de-duping work...a little
+  // concerning, why?
+  conf().set(ConfigOptions::getNodeComparisonCoordinateSensitivityKey(), 6);
+
+  // We're not going to remove missing elements, as we want to have as minimal of an impact on
+  // the resulting changeset as possible.
+  ConfigUtils::removeListOpEntry(
+    ConfigOptions::getConflatePreOpsKey(),
+    QString::fromStdString(RemoveMissingElementsVisitor::className()));
+  ConfigUtils::removeListOpEntry(
+    ConfigOptions::getConflatePostOpsKey(),
+    QString::fromStdString(RemoveMissingElementsVisitor::className()));
+  conf().set(ConfigOptions::getConvertBoundingBoxRemoveMissingElementsKey(), false);
+
+  // These don't change between scenarios (or at least we haven't needed to yet).
+  _boundsOpts.loadRefKeepOnlyInsideBounds = false;
+  _boundsOpts.cookieCutKeepOnlyInsideBounds = false;
+  _boundsOpts.changesetRefKeepOnlyInsideBounds = false;
+
+  // turn on for testing only
+  //conf().set(ConfigOptions::getDebugMapsWriteKey(), true);
 }
 
-QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>
-  ChangesetReplacementCreator::_getCombinedFilters(
-    std::shared_ptr<ChainCriterion> nonGeometryFilter)
+void ChangesetReplacementCreator::_parseConfigOpts(
+  const bool lenientBounds, const GeometryTypeCriterion::GeometryType& geometryType)
 {
-  QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr> combinedFilters;
-  LOG_VART(nonGeometryFilter.get());
-  if (nonGeometryFilter)
+  if (!_cleaningEnabled && _conflationEnabled)
   {
-    for (QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>::const_iterator itr =
-         _geometryTypeFilters.begin(); itr != _geometryTypeFilters.end(); ++itr)
+    throw IllegalArgumentException("If conflation is enabled, cleaning cannot be disabled.");
+  }
+
+  // These settings have been are customized for each geometry type and bounds handling preference.
+  // They were derived from small test cases, so we may need to do some tweaking as we encounter
+  // real world data.
+
+  if (geometryType == GeometryTypeCriterion::GeometryType::Point)
+  {
+    if (lenientBounds)
     {
-      combinedFilters[itr.key()] =
-        std::shared_ptr<ChainCriterion>(new ChainCriterion(itr.value(), nonGeometryFilter));
-      LOG_TRACE("New combined filter: " << combinedFilters[itr.key()]->toString());
+      const QString msg = "--lenient-bounds option ignored with point datasets.";
+      if (_geometryFiltersSpecified)
+      {
+        LOG_WARN(msg);
+      }
+      else
+      {
+        LOG_DEBUG(msg);
+      }
     }
+
+    _boundsOpts.loadRefKeepEntireCrossingBounds = false;
+    _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
+    _boundsOpts.loadSecKeepEntireCrossingBounds = false;
+    _boundsOpts.loadSecKeepOnlyInsideBounds = false;
+    _boundsOpts.cookieCutKeepEntireCrossingBounds = false;
+    _boundsOpts.changesetRefKeepEntireCrossingBounds = false;
+    _boundsOpts.changesetSecKeepEntireCrossingBounds = false;
+    _boundsOpts.changesetSecKeepOnlyInsideBounds = true;
+    _boundsOpts.changesetAllowDeletingRefOutsideBounds = true;
+    _boundsOpts.inBoundsStrict = false;
   }
-  else
+  else if (geometryType == GeometryTypeCriterion::GeometryType::Line)
   {
-    if (_geometryTypeFilters.isEmpty())
+    if (lenientBounds)
     {
-      _geometryTypeFilters = _getDefaultGeometryFilters();
-      _linearFilterClassNames =
-        ConflatableElementCriterion::getCriterionClassNamesByGeometryType(
-          GeometryTypeCriterion::GeometryType::Line);
+      _boundsOpts.loadRefKeepEntireCrossingBounds = true;
+      _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = true;
+      _boundsOpts.loadSecKeepEntireCrossingBounds = true;
+      _boundsOpts.loadSecKeepOnlyInsideBounds = false;
+      _boundsOpts.cookieCutKeepEntireCrossingBounds = false;
+      _boundsOpts.changesetRefKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepOnlyInsideBounds = false;
+      _boundsOpts.changesetAllowDeletingRefOutsideBounds = true;
+      _boundsOpts.inBoundsStrict = false;
     }
-    combinedFilters = _geometryTypeFilters;
-  }
-  LOG_VART(combinedFilters.size());
-  return combinedFilters;
-}
+    else
+    {
+      _boundsOpts.loadRefKeepEntireCrossingBounds = true;
+      _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
+      _boundsOpts.loadSecKeepEntireCrossingBounds = false;
+      _boundsOpts.loadSecKeepOnlyInsideBounds = false;
+      _boundsOpts.cookieCutKeepEntireCrossingBounds = false;
+      _boundsOpts.changesetRefKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepOnlyInsideBounds = false;
+      _boundsOpts.changesetAllowDeletingRefOutsideBounds = false;
+      _boundsOpts.inBoundsStrict = false;
 
-OsmMapPtr ChangesetReplacementCreator::_loadRefMap(const QString& input)
-{ 
-  LOG_INFO("Loading reference map: " << input << "...");
+      // Conflate way joining needs to happen later in the post ops for strict linear replacements.
+      // Changing the default ordering of the post ops to accomodate this had detrimental effects
+      // on other conflation. The best location seems to be at the end just before tag truncation.
+      // would like to get rid of this...isn't a foolproof fix by any means if the conflate post
+      // ops end up getting reordered for some reason.
 
-  // We want to alert the user to the fact their ref versions *could* be being populated incorrectly
-  // to avoid difficulties during changeset application at the end. Its likely if they are incorrect
-  // at this point the changeset derivation will fail at the end anyway, but let's warn now to give
-  // the chance to back out earlier.
-  conf().set(ConfigOptions::getReaderWarnOnZeroVersionElementKey(), true);
+      LOG_VART(conf().getList(ConfigOptions::getConflatePostOpsKey()));
+      QStringList conflatePostOps = conf().getList(ConfigOptions::getConflatePostOpsKey());
+      conflatePostOps.removeAll(QString::fromStdString(WayJoinerOp::className()));
+      const int indexOfTagTruncater =
+        conflatePostOps.indexOf(QString::fromStdString(ApiTagTruncateVisitor::className()));
+      conflatePostOps.insert(
+        indexOfTagTruncater - 1, QString::fromStdString(WayJoinerOp::className()));
+      conf().set(ConfigOptions::getConflatePostOpsKey(), conflatePostOps);
+      LOG_VARD(conf().getList(ConfigOptions::getConflatePostOpsKey()));
+    }
+  }
+  else if (geometryType == GeometryTypeCriterion::GeometryType::Polygon)
+  {
+    if (lenientBounds)
+    {
+      _boundsOpts.loadRefKeepEntireCrossingBounds = true;
+      _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
+      _boundsOpts.loadSecKeepEntireCrossingBounds = true;
+      _boundsOpts.loadSecKeepOnlyInsideBounds = false;
+      _boundsOpts.cookieCutKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetRefKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepOnlyInsideBounds = false;
+      _boundsOpts.changesetAllowDeletingRefOutsideBounds = true;
+      _boundsOpts.inBoundsStrict = false;
+    }
+    else
+    {
+      _boundsOpts.loadRefKeepEntireCrossingBounds = true;
+      _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
+      _boundsOpts.loadSecKeepEntireCrossingBounds = false;
+      _boundsOpts.loadSecKeepOnlyInsideBounds = true;
+      _boundsOpts.cookieCutKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetRefKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepEntireCrossingBounds = false;
+      _boundsOpts.changesetSecKeepOnlyInsideBounds = true;
+      _boundsOpts.changesetAllowDeletingRefOutsideBounds = false;
+      _boundsOpts.inBoundsStrict = true;
+    }
+  }
+  else
+  {
+    // shouldn't ever get here
+    throw IllegalArgumentException("Invalid geometry type.");
+  }
+
+  conf().set(
+    ConfigOptions::getChangesetReplacementAllowDeletingReferenceFeaturesOutsideBoundsKey(),
+    _boundsOpts.changesetAllowDeletingRefOutsideBounds);
+
+  LOG_VART(_boundsOpts.loadRefKeepEntireCrossingBounds);
+  LOG_VART(_boundsOpts.loadRefKeepOnlyInsideBounds);
+  LOG_VART(_boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds);
+  LOG_VART(_boundsOpts.loadSecKeepEntireCrossingBounds);
+  LOG_VART(_boundsOpts.loadSecKeepOnlyInsideBounds);
+  LOG_VART(_boundsOpts.cookieCutKeepEntireCrossingBounds);
+  LOG_VART(_boundsOpts.cookieCutKeepOnlyInsideBounds);
+  LOG_VART(_boundsOpts.changesetRefKeepEntireCrossingBounds);
+  LOG_VART(_boundsOpts.changesetRefKeepOnlyInsideBounds);
+  LOG_VART(_boundsOpts.changesetSecKeepEntireCrossingBounds);
+  LOG_VART(_boundsOpts.changesetSecKeepOnlyInsideBounds);
+  LOG_VART(_boundsOpts.changesetAllowDeletingRefOutsideBounds);
+  LOG_VART(_boundsOpts.inBoundsStrict);
+}
+
+QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>
+  ChangesetReplacementCreator::_getDefaultGeometryFilters() const
+{
+  QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr> featureFilters;
+
+  // Unfortunately, trying to process feature types separately by geometry type breaks down when
+  // you have relations with mixed geometry types and/or features that belong to multiple relations
+  // having different geometry types. The single relation with mixed geometry type membership case
+  // is handled with use of the RelationWithGeometryMembersCriterion implementations below. However,
+  // bugs may occur during cropping if, say, a polygon geometry was procesed in the line geometry
+  // processing loop b/c a line and poly belonged to the same geometry. Haven't seen this actual
+  // bug occur yet, but I believe it can...not sure how to prevent it yet.
+  //
+  // Furthermore, if a feature belongs to two relations with different geometry types, it may be
+  // duplicated in the output. This is why we run a de-duplication routine just before changeset
+  // derivation...kind of a band-aid unfortunately :-(
+
+  // The map will get set on this point crit by the RemoveElementsVisitor later on, right before its
+  // needed.
+  ElementCriterionPtr pointCrit(new PointCriterion());
+  std::shared_ptr<RelationWithPointMembersCriterion> relationPointCrit(
+    new RelationWithPointMembersCriterion());
+  relationPointCrit->setAllowMixedChildren(false);
+  OrCriterionPtr pointOr(new OrCriterion(pointCrit, relationPointCrit));
+  featureFilters[GeometryTypeCriterion::GeometryType::Point] = pointOr;
+
+  ElementCriterionPtr lineCrit(new LinearCriterion());
+  std::shared_ptr<RelationWithLinearMembersCriterion> relationLinearCrit(
+    new RelationWithLinearMembersCriterion());
+  relationLinearCrit->setAllowMixedChildren(true);
+  OrCriterionPtr lineOr(new OrCriterion(lineCrit, relationLinearCrit));
+  featureFilters[GeometryTypeCriterion::GeometryType::Line] = lineOr;
+
+  ElementCriterionPtr polyCrit(new PolygonCriterion());
+  std::shared_ptr<RelationWithPolygonMembersCriterion> relationPolyCrit(
+    new RelationWithPolygonMembersCriterion());
+  relationPolyCrit->setAllowMixedChildren(false);
+  OrCriterionPtr polyOr(new OrCriterion(polyCrit, relationPolyCrit));
+  featureFilters[GeometryTypeCriterion::GeometryType::Polygon] = polyOr;
+
+  return featureFilters;
+}
+
+bool ChangesetReplacementCreator::_roadFilterExists() const
+{
+  ElementCriterionPtr lineFilter = _geometryTypeFilters[GeometryTypeCriterion::GeometryType::Line];
+  if (lineFilter)
+  {
+    return
+      lineFilter->toString()
+        .contains(QString::fromStdString(HighwayCriterion::className()).remove("hoot::"));
+  }
+  return false;
+}
+
+QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>
+  ChangesetReplacementCreator::_getCombinedFilters(
+    std::shared_ptr<ChainCriterion> nonGeometryFilter)
+{
+  QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr> combinedFilters;
+  LOG_VART(nonGeometryFilter.get());
+  // TODO: may be able to consolidate this duplicated filter handling code inside the if/else
+  if (nonGeometryFilter)
+  {
+    for (QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>::const_iterator itr =
+         _geometryTypeFilters.begin(); itr != _geometryTypeFilters.end(); ++itr)
+    {
+      GeometryTypeCriterion::GeometryType geomType = itr.key();
+      LOG_VART(GeometryTypeCriterion::typeToString(geomType));
+      ElementCriterionPtr geometryCrit = itr.value();
+
+      // Roundabouts are classified in hoot as a poly type due to being closed ways. We want to
+      // make sure they get procesed with the linear features, however, and not with the polys. If
+      // they don't, they won't get snapped back to other roads in the output. So if roads were part
+      // of the specified geometry filter, we'll move roundabouts from the poly to the linear
+      // filter. If no roads were specified in the geometry filter, then roads have been explicitly
+      // excluded and we do nothing here. So far this is the only instance where a geometry
+      // re-classification for a feature is necessary. If other instances of this occur things could
+      // get messy really quick, but we'll only worry about that if it actually happens.
+
+      ElementCriterionPtr updatedGeometryCrit;
+      LOG_VART(_roadFilterExists());
+      if (_roadFilterExists())
+      {
+        if (geomType == GeometryTypeCriterion::GeometryType::Line)
+        {
+          LOG_TRACE("Adding roundabouts to line filter due to presence of road filter...");
+          updatedGeometryCrit.reset(
+            new OrCriterion(
+              geometryCrit, std::shared_ptr<RoundaboutCriterion>(new RoundaboutCriterion())));
+        }
+        else if (geomType == GeometryTypeCriterion::GeometryType::Polygon)
+        {
+          LOG_TRACE("Removing roundabouts from polygon filter due to presence of road filter...");
+          updatedGeometryCrit.reset(
+            new ChainCriterion(
+              geometryCrit,
+              NotCriterionPtr(
+                new NotCriterion(
+                  std::shared_ptr<RoundaboutCriterion>(new RoundaboutCriterion())))));
+        }
+      }
+      else
+      {
+        updatedGeometryCrit = geometryCrit;
+      }
+      LOG_VART(updatedGeometryCrit->toString());
+
+      combinedFilters[geomType] =
+        ChainCriterionPtr(new ChainCriterion(updatedGeometryCrit, nonGeometryFilter));
+      LOG_TRACE("New combined filter: " << combinedFilters[geomType]->toString());
+    }
+  }
+  else
+  {
+    LOG_VART(_geometryTypeFilters.size());
+    if (_geometryTypeFilters.isEmpty())
+    {
+      _geometryTypeFilters = _getDefaultGeometryFilters();
+      // It should be ok that the roundabout filter doesn't get added here, since this list is only
+      // for by unconnected way snapping and roundabouts don't fall into that category.
+      _linearFilterClassNames =
+        ConflatableElementCriterion::getCriterionClassNamesByGeometryType(
+          GeometryTypeCriterion::GeometryType::Line);
+    }
+
+    for (QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>::const_iterator itr =
+         _geometryTypeFilters.begin(); itr != _geometryTypeFilters.end(); ++itr)
+    {
+      GeometryTypeCriterion::GeometryType geomType = itr.key();
+      LOG_VART(GeometryTypeCriterion::typeToString(geomType));
+      ElementCriterionPtr geometryCrit = itr.value();
+
+      // See roundabouts handling note in the preceding if statement for more detail. Here we're
+      // doing the same thing, except we don't care if a road filter was specified or not since this
+      // block of code only gets executed if no geometry filters were specified at all and we're
+      // using the defaults.
+
+      ElementCriterionPtr updatedGeometryCrit;
+      if (geomType == GeometryTypeCriterion::GeometryType::Line)
+      {
+        LOG_TRACE("Adding roundabouts to line filter...");
+        updatedGeometryCrit.reset(
+          new OrCriterion(
+            geometryCrit, std::shared_ptr<RoundaboutCriterion>(new RoundaboutCriterion())));
+      }
+      else if (geomType == GeometryTypeCriterion::GeometryType::Polygon)
+      {
+        LOG_TRACE("Removing roundabouts from polygon filter...");
+        updatedGeometryCrit.reset(
+          new ChainCriterion(
+            geometryCrit,
+            NotCriterionPtr(
+              new NotCriterion(
+                std::shared_ptr<RoundaboutCriterion>(new RoundaboutCriterion())))));
+      }
+      else
+      {
+        updatedGeometryCrit = geometryCrit;
+      }
+      LOG_VART(updatedGeometryCrit->toString());
+
+      combinedFilters[geomType] = updatedGeometryCrit;
+      LOG_TRACE("New combined filter: " << combinedFilters[geomType]->toString());
+    }
+  }
+  LOG_VART(combinedFilters.size());
+  return combinedFilters;
+}
+
+OsmMapPtr ChangesetReplacementCreator::_loadRefMap(const QString& input)
+{ 
+  LOG_INFO("Loading reference map: " << input << "...");
+
+  // We want to alert the user to the fact their ref versions *could* be being populated incorrectly
+  // to avoid difficulties during changeset application at the end. Its likely if they are incorrect
+  // at this point the changeset derivation will fail at the end anyway, but let's warn now to give
+  // the chance to back out earlier.
+  conf().set(ConfigOptions::getReaderWarnOnZeroVersionElementKey(), true);
 
   conf().set(
     ConfigOptions::getConvertBoundingBoxKeepEntireFeaturesCrossingBoundsKey(),
@@ -722,7 +1138,10 @@ OsmMapPtr ChangesetReplacementCreator::_loadRefMap(const QString& input)
     ConfigOptions::getConvertBoundingBoxKeepImmediatelyConnectedWaysOutsideBoundsKey(),
     _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds);
 
-  OsmMapPtr refMap(new OsmMap());
+  // Here and with sec map loading, attempted to cache the initial map to avoid unnecessary
+  // reloading, but it wreaked havoc on the element IDs. May try doing it again later.
+  OsmMapPtr refMap;
+  refMap.reset(new OsmMap());
   refMap->setName("ref");
   IoUtils::loadMap(refMap, input, true, Status::Unknown1);
 
@@ -734,54 +1153,6 @@ OsmMapPtr ChangesetReplacementCreator::_loadRefMap(const QString& input)
   return refMap;
 }
 
-QMap<ElementId, long> ChangesetReplacementCreator::_getIdToVersionMappings(
-  const OsmMapPtr& map) const
-{
-  LOG_DEBUG("Mapping element IDs to element versions for: " << map->getName() << "...");
-
-  ElementIdToVersionMapper idToVersionMapper;
-  LOG_STATUS("\t" << idToVersionMapper.getInitStatusMessage());
-  idToVersionMapper.apply(map);
-  LOG_STATUS("\t" << idToVersionMapper.getCompletedStatusMessage());
-  const QMap<ElementId, long> idToVersionMappings = idToVersionMapper.getMappings();
-  LOG_VART(idToVersionMappings.size());
-  return idToVersionMappings;
-}
-
-void ChangesetReplacementCreator::_addChangesetDeleteExclusionTags(OsmMapPtr& map)
-{
-  LOG_INFO(
-    "Setting connected way features outside of bounds to be excluded from deletion for: " <<
-    map->getName() << "...");
-
-  // Add the changeset deletion exclusion tag to all connected ways previously tagged upon load.
-
-  SetTagValueVisitor addTagVis(MetadataTags::HootChangeExcludeDelete(), "yes");
-  LOG_STATUS("\t" << addTagVis.getInitStatusMessage());
-  ChainCriterion addTagCrit(
-    std::shared_ptr<WayCriterion>(new WayCriterion()),
-    std::shared_ptr<TagKeyCriterion>(
-      new TagKeyCriterion(MetadataTags::HootConnectedWayOutsideBounds())));
-  FilteredVisitor deleteExcludeTagVis(addTagCrit, addTagVis);
-  map->visitRw(deleteExcludeTagVis);
-  LOG_STATUS("\t" << addTagVis.getCompletedStatusMessage());
-
-  // Add the changeset deletion exclusion tag to all children of those connected ways.
-
-  std::shared_ptr<ChainCriterion> childAddTagCrit(
-    new ChainCriterion(
-      std::shared_ptr<WayCriterion>(new WayCriterion()),
-      std::shared_ptr<TagKeyCriterion>(
-        new TagKeyCriterion(MetadataTags::HootChangeExcludeDelete()))));
-  RecursiveSetTagValueOp childDeletionExcludeTagOp(
-    MetadataTags::HootChangeExcludeDelete(), "yes", childAddTagCrit);
-  LOG_STATUS("\t" << childDeletionExcludeTagOp.getInitStatusMessage());
-  childDeletionExcludeTagOp.apply(map);
-  LOG_STATUS("\t" << childDeletionExcludeTagOp.getCompletedStatusMessage());
-
-  OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-after-delete-exclusion-tagging");
-}
-
 OsmMapPtr ChangesetReplacementCreator::_loadSecMap(const QString& input)
 {
   LOG_INFO("Loading secondary map: " << input << "...");
@@ -795,7 +1166,8 @@ OsmMapPtr ChangesetReplacementCreator::_loadSecMap(const QString& input)
   conf().set(
     ConfigOptions::getConvertBoundingBoxKeepImmediatelyConnectedWaysOutsideBoundsKey(), false);
 
-  OsmMapPtr secMap(new OsmMap());
+  OsmMapPtr secMap;
+  secMap.reset(new OsmMap());
   secMap->setName("sec");
   IoUtils::loadMap(secMap, input, false, Status::Unknown2);
 
@@ -805,6 +1177,22 @@ OsmMapPtr ChangesetReplacementCreator::_loadSecMap(const QString& input)
   return secMap;
 }
 
+void ChangesetReplacementCreator::_markElementsWithMissingChildren(OsmMapPtr& map)
+{
+  ReportMissingElementsVisitor elementMarker;
+  // Originally, this was going to add reviews rather than tagging elements but there was an ID
+  // provenance problem with reviews.
+  elementMarker.setMarkRelationsForReview(false);
+  elementMarker.setMarkWaysForReview(false);
+  elementMarker.setRelationKvp(MetadataTags::HootMissingChild() + "=yes");
+  elementMarker.setWayKvp(MetadataTags::HootMissingChild() + "=yes");
+  LOG_STATUS("\t" << elementMarker.getInitStatusMessage());
+  map->visitRelationsRw(elementMarker);
+  LOG_STATUS("\t" << elementMarker.getCompletedStatusMessage());
+
+  OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-after-missing-marked");
+}
+
 void ChangesetReplacementCreator::_filterFeatures(
   OsmMapPtr& map, const ElementCriterionPtr& featureFilter, const Settings& config,
   const QString& debugFileName)
@@ -813,12 +1201,15 @@ void ChangesetReplacementCreator::_filterFeatures(
     "Filtering features for: " << map->getName() << " based on input filter: " +
     featureFilter->toString() << "...");
 
+  // Negate the input filter, since we're removing everything but what passes the input filter.
   RemoveElementsVisitor elementPruner(true);
   // The criteria must be added before the config or map is set. We may want to change
   // MultipleCriterionConsumerVisitor and RemoveElementsVisitor to make this behavior less brittle.
   elementPruner.addCriterion(featureFilter);
   elementPruner.setConfiguration(config);
   elementPruner.setOsmMap(map.get());
+  // If recursion isn't used here, nasty crashes that are hard to track down occur at times. I'm
+  // not completely convinced recursion should be used here, though.
   elementPruner.setRecursive(true);
   LOG_STATUS("\t" << elementPruner.getInitStatusMessage());
   map->visitRw(elementPruner);
@@ -875,7 +1266,7 @@ OsmMapPtr ChangesetReplacementCreator::_getCookieCutMap(
   OsmMapPtr cutterMapToUse;
   LOG_VART(cutterMap->getElementCount());
   ConfigOptions opts(conf());
-  LOG_VART(OsmUtils::mapIsPointsOnly(cutterMap));
+  LOG_VART(MapUtils::mapIsPointsOnly(cutterMap));
   const double cookieCutterAlpha = opts.getCookieCutterAlpha();
   double cookieCutterAlphaShapeBuffer = opts.getCookieCutterAlphaShapeBuffer();
   LOG_VART(_fullReplacement);
@@ -885,30 +1276,34 @@ OsmMapPtr ChangesetReplacementCreator::_getCookieCutMap(
     // replaced.
     LOG_DEBUG("Using dough map as cutter shape map...");
     cutterMapToUse = doughMap;
+    // TODO: riverbank test fails with missing POIs without this and the single point test has
+    // extra POIs in output without this; explain
     cookieCutterAlphaShapeBuffer = 10.0;
   }
-  else if (cutterMap->getElementCount() < 3 && OsmUtils::mapIsPointsOnly(cutterMap))
+  else
   {
-    // Generate a cutter shape based on a transformation of the cropped secondary map.
-
-    // Found that if a map only has a couple points or less, generating an alpha shape from them may
-    // not be possible (or at least I don't know how to yet). So instead, go through the points in
-    // the map and replace them with small square polys...from that we can generate the alpha shape.
+    // Generate a cutter shape based on the cropped secondary map, which will cause only
+    // overlapping data between the two datasets to be replaced.
+    LOG_DEBUG("Using cutter map as cutter shape map...");
+    cutterMapToUse = cutterMap;
+  }
 
-    LOG_DEBUG("Generating cutter shape map from sec map transformation...");
+  // Found that if a map only has a couple points or less, generating an alpha shape from them may
+  // not be possible (or at least don't know how to yet). So instead, go through the points in
+  // the map and replace them with small square polys...from that we can generate the alpha shape.
+  if ((int)cutterMapToUse->getElementCount() < 3 && MapUtils::mapIsPointsOnly(cutterMapToUse))
+  {
+    LOG_DEBUG("Creating a cutter shape map transformation for point map...");
+    // Make a copy here since we're making destructive changes to the geometry here for alpha shape
+    // generation purposes only.
     cutterMapToUse.reset(new OsmMap(cutterMap));
-    PointsToPolysConverter pointConverter;
+    PointsToPolysConverter pointConverter/*(1.0)*/;
     LOG_STATUS("\t" << pointConverter.getInitStatusMessage());
     pointConverter.apply(cutterMapToUse);
     LOG_STATUS("\t" << pointConverter.getCompletedStatusMessage());
     MapProjector::projectToWgs84(cutterMapToUse);
   }
-  else
-  {
-    // Generate a cutter shape based on the cropped secondary map.
-    LOG_DEBUG("Using cutter map as cutter shape map...");
-    cutterMapToUse = cutterMap;
-  }
+
   LOG_VART(cutterMapToUse->getElementCount());
   OsmMapWriterFactory::writeDebugMap(cutterMapToUse, "cutter-map");
 
@@ -944,23 +1339,75 @@ OsmMapPtr ChangesetReplacementCreator::_getCookieCutMap(
   // Cookie cut the shape of the cutter shape map out of the cropped ref map.
   LOG_INFO("Cookie cutting cutter shape out of: " << cookieCutMap->getName() << "...");
 
+  // We're not going to remove missing elements, as we want to have as minimal of an impact on
+  // the resulting changeset as possible.
   CookieCutter(
     false, 0.0, _boundsOpts.cookieCutKeepEntireCrossingBounds,
-    _boundsOpts.cookieCutKeepOnlyInsideBounds)
+    _boundsOpts.cookieCutKeepOnlyInsideBounds, false)
     .cut(cutterShapeOutlineMap, cookieCutMap);
   MapProjector::projectToWgs84(cookieCutMap); // not exactly sure yet why this needs to be done
   LOG_VARD(cookieCutMap->getElementCount());
   MapProjector::projectToWgs84(doughMap);
   LOG_VART(doughMap->getElementCount());
   LOG_VART(MapProjector::toWkt(cookieCutMap->getProjection()));
+  MemoryUsageChecker::getInstance().check();
   OsmMapWriterFactory::writeDebugMap(cookieCutMap, "cookie-cut");
 
   return cookieCutMap;
 }
 
-void ChangesetReplacementCreator::_combineMaps(OsmMapPtr& map1, OsmMapPtr& map2,
-                                               const bool throwOutDupes,
-                                               const QString& debugFileName)
+QMap<ElementId, long> ChangesetReplacementCreator::_getIdToVersionMappings(
+  const OsmMapPtr& map) const
+{
+  LOG_DEBUG("Mapping element IDs to element versions for: " << map->getName() << "...");
+
+  ElementIdToVersionMapper idToVersionMapper;
+  LOG_STATUS("\t" << idToVersionMapper.getInitStatusMessage());
+  idToVersionMapper.apply(map);
+  MemoryUsageChecker::getInstance().check();
+  LOG_STATUS("\t" << idToVersionMapper.getCompletedStatusMessage());
+  const QMap<ElementId, long> idToVersionMappings = idToVersionMapper.getMappings();
+  LOG_VART(idToVersionMappings.size());
+  return idToVersionMappings;
+}
+
+void ChangesetReplacementCreator::_addChangesetDeleteExclusionTags(OsmMapPtr& map)
+{
+  LOG_INFO(
+    "Setting connected way features outside of bounds to be excluded from deletion for: " <<
+    map->getName() << "...");
+
+  // Add the changeset deletion exclusion tag to all connected ways previously tagged upon load.
+
+  SetTagValueVisitor addTagVis(MetadataTags::HootChangeExcludeDelete(), "yes");
+  LOG_STATUS("\t" << addTagVis.getInitStatusMessage());
+  ChainCriterion addTagCrit(
+    std::shared_ptr<WayCriterion>(new WayCriterion()),
+    std::shared_ptr<TagKeyCriterion>(
+      new TagKeyCriterion(MetadataTags::HootConnectedWayOutsideBounds())));
+  FilteredVisitor deleteExcludeTagVis(addTagCrit, addTagVis);
+  map->visitRw(deleteExcludeTagVis);
+  LOG_STATUS("\t" << addTagVis.getCompletedStatusMessage());
+
+  // Add the changeset deletion exclusion tag to all children of those connected ways.
+
+  std::shared_ptr<ChainCriterion> childAddTagCrit(
+    new ChainCriterion(
+      std::shared_ptr<WayCriterion>(new WayCriterion()),
+      std::shared_ptr<TagKeyCriterion>(
+        new TagKeyCriterion(MetadataTags::HootChangeExcludeDelete()))));
+  RecursiveSetTagValueOp childDeletionExcludeTagOp(
+    MetadataTags::HootChangeExcludeDelete(), "yes", childAddTagCrit);
+  LOG_STATUS("\t" << childDeletionExcludeTagOp.getInitStatusMessage());
+  childDeletionExcludeTagOp.apply(map);
+  LOG_STATUS("\t" << childDeletionExcludeTagOp.getCompletedStatusMessage());
+
+  MemoryUsageChecker::getInstance().check();
+  OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-after-delete-exclusion-tagging");
+}
+
+void ChangesetReplacementCreator::_combineMaps(
+  OsmMapPtr& map1, OsmMapPtr& map2, const bool throwOutDupes, const QString& debugFileName)
 {
   LOG_VART(map1.get());
   LOG_VART(map2.get());
@@ -980,6 +1427,7 @@ void ChangesetReplacementCreator::_combineMaps(OsmMapPtr& map1, OsmMapPtr& map2,
   LOG_VART(MapProjector::toWkt(map1->getProjection()));
   LOG_DEBUG("Combined map size: " << map1->size());
 
+  MemoryUsageChecker::getInstance().check();
   OsmMapWriterFactory::writeDebugMap(map1, debugFileName);
 }
 
@@ -1001,6 +1449,11 @@ void ChangesetReplacementCreator::_conflate(OsmMapPtr& map, const bool lenientBo
   }
   conf().set(ConfigOptions::getWayJoinerAdvancedStrictNameMatchKey(), !_isNetworkConflate());
 
+  if (ConfigOptions().getConflateRemoveSuperfluousOps())
+  {
+    SuperfluousConflateOpRemover::removeSuperfluousOps();
+  }
+
   NamedOp preOps(ConfigOptions().getConflatePreOps());
   preOps.apply(map);
 
@@ -1015,6 +1468,46 @@ void ChangesetReplacementCreator::_conflate(OsmMapPtr& map, const bool lenientBo
   LOG_DEBUG("Conflated map size: " << map->size());
 }
 
+void ChangesetReplacementCreator::_removeConflateReviews(OsmMapPtr& map)
+{
+  LOG_INFO("Removing reviews added during conflation from " << map->getName() << "...");
+
+  RemoveElementsVisitor removeVis;
+  removeVis.addCriterion(ElementCriterionPtr(new RelationCriterion("review")));
+  removeVis.addCriterion(
+    ElementCriterionPtr(
+      new NotCriterion(
+        std::shared_ptr<TagCriterion>(
+          new TagCriterion(
+            MetadataTags::HootReviewType(),
+            QString::fromStdString(ReportMissingElementsVisitor::className()))))));
+  removeVis.setChainCriteria(true);
+  removeVis.setRecursive(false);
+  LOG_STATUS("\t" << removeVis.getInitStatusMessage());
+  map->visitRw(removeVis);
+  LOG_STATUS("\t" << removeVis.getCompletedStatusMessage());
+
+  MemoryUsageChecker::getInstance().check();
+  LOG_VART(MapProjector::toWkt(map->getProjection()));
+  OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-conflate-reviews-removed");
+}
+
+void ChangesetReplacementCreator::_clean(OsmMapPtr& map)
+{
+  map->setName("cleaned");
+  LOG_INFO(
+    "Cleaning the combined cookie cut reference and secondary maps: " << map->getName() << "...");
+
+  // TODO: since we're never conflating when we call clean, should we remove cleaning ops like
+  // IntersectionSplitter?
+  MapCleaner().apply(map);
+
+  MapProjector::projectToWgs84(map);  // cleaning works in planar
+  LOG_VART(MapProjector::toWkt(map->getProjection()));
+  OsmMapWriterFactory::writeDebugMap(map, "cleaned");
+  LOG_DEBUG("Cleaned map size: " << map->size());
+}
+
 void ChangesetReplacementCreator::_snapUnconnectedWays(
   OsmMapPtr& map, const QStringList& snapWayStatuses, const QStringList& snapToWayStatuses,
   const QString& typeCriterionClassName, const bool markSnappedWays, const QString& debugFileName)
@@ -1041,7 +1534,7 @@ void ChangesetReplacementCreator::_snapUnconnectedWays(
 
   MapProjector::projectToWgs84(map);   // snapping works in planar
   LOG_VART(MapProjector::toWkt(map->getProjection()));
-
+  MemoryUsageChecker::getInstance().check();
   OsmMapWriterFactory::writeDebugMap(map, debugFileName);
 }
 
@@ -1058,16 +1551,17 @@ OsmMapPtr ChangesetReplacementCreator::_getImmediatelyConnectedOutOfBoundsWays(
       std::shared_ptr<WayCriterion>(new WayCriterion()),
       std::shared_ptr<TagKeyCriterion>(
         new TagKeyCriterion(MetadataTags::HootConnectedWayOutsideBounds()))));
-  OsmMapPtr connectedWays = OsmUtils::getMapSubset(map, copyCrit);
+  OsmMapPtr connectedWays = MapUtils::getMapSubset(map, copyCrit);
   connectedWays->setName(outputMapName);
   LOG_VART(MapProjector::toWkt(connectedWays->getProjection()));
+  MemoryUsageChecker::getInstance().check();
   OsmMapWriterFactory::writeDebugMap(connectedWays, "connected-ways");
   return connectedWays;
 }
 
 void ChangesetReplacementCreator::_cropMapForChangesetDerivation(
   OsmMapPtr& map, const geos::geom::Envelope& bounds, const bool keepEntireFeaturesCrossingBounds,
-  const bool keepOnlyFeaturesInsideBounds, const bool isLinearMap, const QString& debugFileName)
+  const bool keepOnlyFeaturesInsideBounds, const QString& debugFileName)
 {
   if (map->size() == 0)
   {
@@ -1081,16 +1575,14 @@ void ChangesetReplacementCreator::_cropMapForChangesetDerivation(
   MapCropper cropper(bounds);
   cropper.setKeepEntireFeaturesCrossingBounds(keepEntireFeaturesCrossingBounds);
   cropper.setKeepOnlyFeaturesInsideBounds(keepOnlyFeaturesInsideBounds);
+  // We're not going to remove missing elements, as we want to have as minimal of an impact on
+  // the resulting changeset as possible.
+  cropper.setRemoveMissingElements(false);
   LOG_STATUS("\t" << cropper.getInitStatusMessage());
   cropper.apply(map);
   LOG_STATUS("\t" << cropper.getCompletedStatusMessage());
 
-  // Clean up straggling nodes in that are the result of cropping. Its ok to ignore info tags when
-  // dealing with only linear features, as all nodes in the data being conflated should be way nodes
-  // with no information.
-  // TODO: This can be removed now, since its already happening in MapCropper, right?
-  SuperfluousNodeRemover::removeNodes(map, isLinearMap);
-
+  MemoryUsageChecker::getInstance().check();
   LOG_VART(MapProjector::toWkt(map->getProjection()));
   OsmMapWriterFactory::writeDebugMap(map, debugFileName);
   LOG_DEBUG("Cropped map: " << map->getName() << " size: " << map->size());
@@ -1118,12 +1610,13 @@ void ChangesetReplacementCreator::_removeUnsnappedImmediatelyConnectedOutOfBound
   map->visitRw(removeVis);
   LOG_STATUS("\t" << removeVis.getCompletedStatusMessage());
 
+  MemoryUsageChecker::getInstance().check();
   LOG_VART(MapProjector::toWkt(map->getProjection()));
   OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-unsnapped-removed");
 }
 
-void ChangesetReplacementCreator::_excludeFeaturesFromChangesetDeletion(OsmMapPtr& map,
-                                                                        const QString& boundsStr)
+void ChangesetReplacementCreator::_excludeFeaturesFromChangesetDeletion(
+  OsmMapPtr& map, const QString& boundsStr)
 {
   if (map->size() == 0)
   {
@@ -1145,169 +1638,91 @@ void ChangesetReplacementCreator::_excludeFeaturesFromChangesetDeletion(OsmMapPt
   tagSetter.apply(map);
   LOG_STATUS("\t" << tagSetter.getCompletedStatusMessage());
 
+  MemoryUsageChecker::getInstance().check();
   LOG_VART(MapProjector::toWkt(map->getProjection()));
   OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-after-delete-exclude-tags");
 }
 
-bool ChangesetReplacementCreator::_isNetworkConflate() const
-{
-  return
-    ConfigOptions().getMatchCreators().contains(
-      QString::fromStdString(NetworkMatchCreator::className()));
-}
-
-void ChangesetReplacementCreator::_setGlobalOpts(const QString& boundsStr)
-{
-  conf().set(ConfigOptions::getChangesetXmlWriterAddTimestampKey(), false);
-  conf().set(ConfigOptions::getReaderAddSourceDatetimeKey(), false);
-  conf().set(ConfigOptions::getWriterIncludeCircularErrorTagsKey(), false);
-  conf().set(ConfigOptions::getConvertBoundingBoxKey(), boundsStr);
-  // For this being enabled to have any effect,
-  // convert.bounding.box.keep.immediately.connected.ways.outside.bounds must be enabled as well.
-  conf().set(
-    ConfigOptions::getConvertBoundingBoxTagImmediatelyConnectedOutOfBoundsWaysKey(),
-    _tagOobConnectedWays);
-  // will have to see if setting this to false causes problems in the future...
-  conf().set(ConfigOptions::getConvertRequireAreaForPolygonKey(), false);
-  // turn on for testing only
-  //conf().set(ConfigOptions::getDebugMapsWriteKey(), true);
-
-  // These don't change between scenarios (or at least haven't needed to yet).
-  _boundsOpts.loadRefKeepOnlyInsideBounds = false;
-  _boundsOpts.cookieCutKeepOnlyInsideBounds = false;
-  _boundsOpts.changesetRefKeepOnlyInsideBounds = false;
-}
-
-void ChangesetReplacementCreator::_parseConfigOpts(
-  const bool lenientBounds, const GeometryTypeCriterion::GeometryType& geometryType)
+void ChangesetReplacementCreator::_dedupeMaps(const QList<OsmMapPtr>& maps)
 {
-  // These settings have been are customized for each geometry type and bounds handling preference.
-  // They were derived from small test cases, so we may need to do some tweaking as we encounter
-  // real world data.
-
-  if (geometryType == GeometryTypeCriterion::GeometryType::Point)
+  ElementDeduplicator deduper;
+  // intra-map de-duping breaks the roundabouts test when ref maps are de-duped
+  deduper.setDedupeIntraMap(true);
+  // When nodes are removed (cleaned/conflated only), out of spec, single point, and riverbank tests
+  // fail, so being a little more strict by removing points instead (node + not a way node).
+  std::shared_ptr<PointCriterion> pointCrit(new PointCriterion());
+  deduper.setNodeCriterion(pointCrit);
+  // this prevents connected ways separated by geometry type from being broken up in the output
+  deduper.setFavorMoreConnectedWays(true);
+
+  // See notes in _getDefaultGeometryFilters, but basically the point and poly geometry maps may
+  // have duplicates and the line geometry map will not. So dedupe each of the others compared to
+  // the line map.
+
+  OsmMapPtr lineMap;
+  QList<OsmMapPtr> otherMaps;
+  for (int i = 0; i < maps.size(); i++)
   {
-    if (lenientBounds)
+    OsmMapPtr map = maps.at(i);
+    if (map->getName().contains("line"))
     {
-      const QString msg = "--lenient-bounds option ignored with point datasets.";
-      if (_geometryFiltersSpecified)
-      {
-        LOG_WARN(msg);
-      }
-      else
-      {
-        LOG_DEBUG(msg);
-      }
-    }
-
-    _boundsOpts.loadRefKeepEntireCrossingBounds = false;
-    _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
-    _boundsOpts.loadSecKeepEntireCrossingBounds = false;
-    _boundsOpts.loadSecKeepOnlyInsideBounds = false;
-    _boundsOpts.cookieCutKeepEntireCrossingBounds = false;
-    _boundsOpts.changesetRefKeepEntireCrossingBounds = false;
-    _boundsOpts.changesetSecKeepEntireCrossingBounds = false;
-    _boundsOpts.changesetSecKeepOnlyInsideBounds = true;
-    _boundsOpts.changesetAllowDeletingRefOutsideBounds = true;
-    _boundsOpts.inBoundsStrict = false;
-  }
-  else if (geometryType == GeometryTypeCriterion::GeometryType::Line)
-  {
-    if (lenientBounds)
-    {
-      _boundsOpts.loadRefKeepEntireCrossingBounds = true;
-      _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = true;
-      _boundsOpts.loadSecKeepEntireCrossingBounds = true;
-      _boundsOpts.loadSecKeepOnlyInsideBounds = false;
-      _boundsOpts.cookieCutKeepEntireCrossingBounds = false;
-      _boundsOpts.changesetRefKeepEntireCrossingBounds = true;
-      _boundsOpts.changesetSecKeepEntireCrossingBounds = true;
-      _boundsOpts.changesetSecKeepOnlyInsideBounds = false;
-      _boundsOpts.changesetAllowDeletingRefOutsideBounds = true;
-      _boundsOpts.inBoundsStrict = false;
+      lineMap = map;
     }
     else
     {
-      _boundsOpts.loadRefKeepEntireCrossingBounds = true;
-      _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
-      _boundsOpts.loadSecKeepEntireCrossingBounds = false;
-      _boundsOpts.loadSecKeepOnlyInsideBounds = false;
-      _boundsOpts.cookieCutKeepEntireCrossingBounds = false;
-      _boundsOpts.changesetRefKeepEntireCrossingBounds = true;
-      _boundsOpts.changesetSecKeepEntireCrossingBounds = true;
-      _boundsOpts.changesetSecKeepOnlyInsideBounds = false;
-      _boundsOpts.changesetAllowDeletingRefOutsideBounds = false;
-      _boundsOpts.inBoundsStrict = false;
-
-      // Conflate way joining needs to happen later in the post ops for strict linear replacements.
-      // Changing the default ordering of the post ops to accomodate this had detrimental effects
-      // on other conflation. The best location seems to be at the end just before tag truncation.
-      // would like to get rid of this...isn't a foolproof fix by any means if the conflate post
-      // ops end up getting reordered for some reason.
-
-      LOG_VART(conf().getList(ConfigOptions::getConflatePostOpsKey()));
-      QStringList conflatePostOps = conf().getList(ConfigOptions::getConflatePostOpsKey());
-      conflatePostOps.removeAll(QString::fromStdString(WayJoinerOp::className()));
-      const int indexOfTagTruncater =
-        conflatePostOps.indexOf(QString::fromStdString(ApiTagTruncateVisitor::className()));
-      conflatePostOps.insert(
-        indexOfTagTruncater - 1, QString::fromStdString(WayJoinerOp::className()));
-      conf().set(ConfigOptions::getConflatePostOpsKey(), conflatePostOps);
-      LOG_VARD(conf().getList(ConfigOptions::getConflatePostOpsKey()));
+      otherMaps.append(map);
     }
   }
-  else if (geometryType == GeometryTypeCriterion::GeometryType::Polygon)
-  {
-    if (lenientBounds)
-    {
-      _boundsOpts.loadRefKeepEntireCrossingBounds = true;
-      _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
-      _boundsOpts.loadSecKeepEntireCrossingBounds = true;
-      _boundsOpts.loadSecKeepOnlyInsideBounds = false;
-      _boundsOpts.cookieCutKeepEntireCrossingBounds = true;
-      _boundsOpts.changesetRefKeepEntireCrossingBounds = true;
-      _boundsOpts.changesetSecKeepEntireCrossingBounds = true;
-      _boundsOpts.changesetSecKeepOnlyInsideBounds = false;
-      _boundsOpts.changesetAllowDeletingRefOutsideBounds = true;
-      _boundsOpts.inBoundsStrict = false;
-    }
-    else
-    {
-      _boundsOpts.loadRefKeepEntireCrossingBounds = true;
-      _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
-      _boundsOpts.loadSecKeepEntireCrossingBounds = false;
-      _boundsOpts.loadSecKeepOnlyInsideBounds = true;
-      _boundsOpts.cookieCutKeepEntireCrossingBounds = true;
-      _boundsOpts.changesetRefKeepEntireCrossingBounds = true;
-      _boundsOpts.changesetSecKeepEntireCrossingBounds = false;
-      _boundsOpts.changesetSecKeepOnlyInsideBounds = true;
-      _boundsOpts.changesetAllowDeletingRefOutsideBounds = false;
-      _boundsOpts.inBoundsStrict = true;
-    }
-  }
-  else
+
+  for (int i = 0; i < otherMaps.size(); i++)
   {
-    // shouldn't ever get here
-    throw IllegalArgumentException("Invalid geometry type.");
+    OsmMapPtr otherMap = otherMaps.at(i);
+    // set the point's map to be the map we're removing features from
+    pointCrit->setOsmMap(otherMap.get());
+    LOG_DEBUG(
+      "De-duping map: " << lineMap->getName() << " and " << otherMap->getName() << "...");
+    deduper.dedupe(lineMap, otherMap);
   }
+}
 
-  conf().set(
-    ConfigOptions::getChangesetReplacementAllowDeletingReferenceFeaturesOutsideBoundsKey(),
-    _boundsOpts.changesetAllowDeletingRefOutsideBounds);
+void ChangesetReplacementCreator::_cleanup(OsmMapPtr& map)
+{
+  LOG_INFO("Cleaning up missing elements for " << map->getName() << "...");
 
-  LOG_VART(_boundsOpts.loadRefKeepEntireCrossingBounds);
-  LOG_VART(_boundsOpts.loadRefKeepOnlyInsideBounds);
-  LOG_VART(_boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds);
-  LOG_VART(_boundsOpts.loadSecKeepEntireCrossingBounds);
-  LOG_VART(_boundsOpts.loadSecKeepOnlyInsideBounds);
-  LOG_VART(_boundsOpts.cookieCutKeepEntireCrossingBounds);
-  LOG_VART(_boundsOpts.cookieCutKeepOnlyInsideBounds);
-  LOG_VART(_boundsOpts.changesetRefKeepEntireCrossingBounds);
-  LOG_VART(_boundsOpts.changesetRefKeepOnlyInsideBounds);
-  LOG_VART(_boundsOpts.changesetSecKeepEntireCrossingBounds);
-  LOG_VART(_boundsOpts.changesetSecKeepOnlyInsideBounds);
-  LOG_VART(_boundsOpts.changesetAllowDeletingRefOutsideBounds);
-  LOG_VART(_boundsOpts.inBoundsStrict);
+  // This will handle removing refs in relation members we've cropped out.
+//  RemoveMissingElementsVisitor missingElementsRemover;
+//  LOG_STATUS("\t" << missingElementsRemover.getInitStatusMessage());
+//  map->visitRw(missingElementsRemover);
+//  LOG_STATUS("\t" << missingElementsRemover.getCompletedStatusMessage());
+
+  // Due to mixed geometry type relations explained in _getDefaultGeometryFilters, we may have
+  // introduced some duplicate relation members.
+  RemoveDuplicateRelationMembersVisitor dupeMembersRemover;
+  LOG_STATUS("\t" << dupeMembersRemover.getInitStatusMessage());
+  map->visitRw(dupeMembersRemover);
+  LOG_STATUS("\t" << dupeMembersRemover.getCompletedStatusMessage());
+
+  // get rid of straggling nodes
+  SuperfluousNodeRemover orphanedNodeRemover;
+  LOG_STATUS("\t" << orphanedNodeRemover.getInitStatusMessage());
+  orphanedNodeRemover.apply(map);
+  LOG_STATUS("\t" << orphanedNodeRemover.getCompletedStatusMessage());
+
+  // This will remove any relations that were already empty or became empty after we removed missing
+  // members.
+  RemoveEmptyRelationsOp emptyRelationRemover;
+  LOG_STATUS("\t" << emptyRelationRemover.getInitStatusMessage());
+  emptyRelationRemover.apply(map);
+  LOG_STATUS("\t" << emptyRelationRemover.getCompletedStatusMessage());
+
+  MapProjector::projectToWgs84(map);
+}
+
+bool ChangesetReplacementCreator::_isNetworkConflate() const
+{
+  return
+    ConfigOptions().getMatchCreators().contains(
+      QString::fromStdString(NetworkMatchCreator::className()));
 }
 
 }
Clone this wiki locally