diff --git a/conf/core/ConfigOptions.asciidoc b/conf/core/ConfigOptions.asciidoc
index 685450b470..ee2fbfec5a 100644
--- a/conf/core/ConfigOptions.asciidoc
+++ b/conf/core/ConfigOptions.asciidoc
@@ -5501,6 +5501,13 @@ shape file and cannot be used in combination with a translation. If this option
then the list of columns will be automatically determined based on the most frequently populated
tags.
+=== shift.nodes.xy
+
+* Data Type: string
+* Default Value: `0.0,0.0`
+
+Shift all nodes in the map by x, y meters when using the `NodesShiftVisitor`
+
=== small.disconnected.way.remover.max.node.count
* Data Type: int
diff --git a/hoot-core-test/src/test/cpp/hoot/core/visitors/NodesShiftVisitorTest.cpp b/hoot-core-test/src/test/cpp/hoot/core/visitors/NodesShiftVisitorTest.cpp
new file mode 100644
index 0000000000..eb837178d7
--- /dev/null
+++ b/hoot-core-test/src/test/cpp/hoot/core/visitors/NodesShiftVisitorTest.cpp
@@ -0,0 +1,106 @@
+/*
+ * This file is part of Hootenanny.
+ *
+ * Hootenanny is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * --------------------------------------------------------------------
+ *
+ * The following copyright notices are generated automatically. If you
+ * have a new notice to add, please use the format:
+ * " * @copyright Copyright ..."
+ * This will properly maintain the copyright information. Maxar
+ * copyrights will be updated automatically.
+ *
+ * @copyright Copyright (C) 2023 Maxar (http://www.maxar.com/)
+ */
+
+// Hoot
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace hoot
+{
+
+class NodesShiftVisitorTest : public HootTestFixture
+{
+ CPPUNIT_TEST_SUITE(NodesShiftVisitorTest);
+ CPPUNIT_TEST(singleNodeApplyTest);
+ CPPUNIT_TEST(applyToMapTest);
+ CPPUNIT_TEST_SUITE_END();
+
+public:
+
+ NodesShiftVisitorTest()
+ : HootTestFixture("test-files/visitors/NodesShiftVisitorTest/",
+ "test-output/visitors/NodesShiftVisitorTest/")
+ {
+ }
+
+ void singleNodeApplyTest()
+ {
+ std::shared_ptr n = std::make_shared(Status::Unknown1, 1, 0.0, 0.0);
+ // Positive moves
+ NodesShiftVisitor visitor(10.0, 12.0);
+ visitor.visit(n);
+ CPPUNIT_ASSERT_EQUAL(12.0, n->getX());
+ CPPUNIT_ASSERT_EQUAL(10.0, n->getY());
+ // Negative moves
+ visitor.setShift(-2.0, -100.0);
+ visitor.visit(n);
+ CPPUNIT_ASSERT_EQUAL(-88.0, n->getX());
+ CPPUNIT_ASSERT_EQUAL(8.0, n->getY());
+ }
+
+ void applyToMapTest()
+ {
+ OsmMapPtr map = std::make_shared();
+ OsmMapReaderFactory::read(map, "test-files/ToyTestA.osm", false, Status::Unknown1);
+ MapProjector::projectToPlanar(map);
+
+ conf().set(ConfigOptions::getShiftNodesXyKey(), "10.0,20.0");
+
+ NodesShiftVisitor uut;
+ map->visitRw(uut);
+
+ MapProjector::projectToWgs84(map);
+ OsmMapWriterFactory::write(map, _outputPath + "applyToMapOutput1.osm");
+
+ HOOT_FILE_EQUALS(_inputPath + "applyToMapExpected1.osm",
+ _outputPath + "applyToMapOutput1.osm");
+
+ map->clear();
+ OsmMapReaderFactory::read(map, "test-files/ToyTestA.osm", false, Status::Unknown1);
+ MapProjector::projectToPlanar(map);
+
+ uut.setShift(-100.0, -55.0);
+
+ map->visitRw(uut);
+
+ MapProjector::projectToWgs84(map);
+ OsmMapWriterFactory::write(map, _outputPath + "applyToMapOutput2.osm");
+
+ HOOT_FILE_EQUALS(_inputPath + "applyToMapExpected2.osm",
+ _outputPath + "applyToMapOutput2.osm");
+
+ conf().set(ConfigOptions::getShiftNodesXyKey(), ConfigOptions::getShiftNodesXyDefaultValue());
+ }
+};
+
+CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(NodesShiftVisitorTest, "quick");
+
+}
diff --git a/hoot-core/src/main/cpp/hoot/core/visitors/NodesShiftVisitor.cpp b/hoot-core/src/main/cpp/hoot/core/visitors/NodesShiftVisitor.cpp
new file mode 100644
index 0000000000..d71d8b0de6
--- /dev/null
+++ b/hoot-core/src/main/cpp/hoot/core/visitors/NodesShiftVisitor.cpp
@@ -0,0 +1,102 @@
+/*
+ * This file is part of Hootenanny.
+ *
+ * Hootenanny is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * --------------------------------------------------------------------
+ *
+ * The following copyright notices are generated automatically. If you
+ * have a new notice to add, please use the format:
+ * " * @copyright Copyright ..."
+ * This will properly maintain the copyright information. Maxar
+ * copyrights will be updated automatically.
+ *
+ * @copyright Copyright (C) 2023 Maxar (http://www.maxar.com/)
+ */
+#include "NodesShiftVisitor.h"
+
+// hoot
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Qt
+#include
+
+using namespace geos::geom;
+using namespace std;
+
+namespace hoot
+{
+
+HOOT_FACTORY_REGISTER(ElementVisitor, NodesShiftVisitor)
+
+NodesShiftVisitor::NodesShiftVisitor()
+ : _northShift(0.0),
+ _eastShift(0.0)
+{
+ _parseShift();
+}
+
+NodesShiftVisitor::NodesShiftVisitor(Meters north_shift, Meters east_shift)
+ : _northShift(north_shift),
+ _eastShift(east_shift)
+{
+}
+
+void NodesShiftVisitor::setShift(Meters north_shift, Meters east_shift)
+{
+ _northShift = north_shift;
+ _eastShift = east_shift;
+}
+
+void NodesShiftVisitor::visit(const std::shared_ptr& e)
+{
+ if (e->getElementType() == ElementType::Node)
+ {
+ NodePtr node = std::dynamic_pointer_cast(e);
+ node->setX(node->getX() + _eastShift);
+ node->setY(node->getY() + _northShift);
+ }
+}
+
+void NodesShiftVisitor::_parseShift()
+{
+ QString shift = ConfigOptions().getShiftNodesXy();
+ QStringList values = shift.split(",");
+ if (values.size() != 2)
+ return;
+ bool goodX = false;
+ bool goodY = false;
+ double x = values[0].toDouble(&goodX);
+ double y = values[1].toDouble(&goodY);
+ if (!goodX || !goodY)
+ return;
+ _northShift = y;
+ _eastShift = x;
+}
+
+}
diff --git a/hoot-core/src/main/cpp/hoot/core/visitors/NodesShiftVisitor.h b/hoot-core/src/main/cpp/hoot/core/visitors/NodesShiftVisitor.h
new file mode 100644
index 0000000000..ec96df56e0
--- /dev/null
+++ b/hoot-core/src/main/cpp/hoot/core/visitors/NodesShiftVisitor.h
@@ -0,0 +1,72 @@
+/*
+ * This file is part of Hootenanny.
+ *
+ * Hootenanny is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * --------------------------------------------------------------------
+ *
+ * The following copyright notices are generated automatically. If you
+ * have a new notice to add, please use the format:
+ * " * @copyright Copyright ..."
+ * This will properly maintain the copyright information. Maxar
+ * copyrights will be updated automatically.
+ *
+ * @copyright Copyright (C) 2023 Maxar (http://www.maxar.com/)
+ */
+#ifndef NODES_SHIFT_VISITOR_H
+#define NODES_SHIFT_VISITOR_H
+
+// hoot
+#include
+#include
+
+namespace hoot
+{
+
+/**
+ * @brief The NodesShiftVisitor class shifts all nodes in a map by Y meters north and X meters east
+ * NOTE: The map must be in planar coordinates for this visitor to work
+ */
+class NodesShiftVisitor : public ElementVisitor
+{
+public:
+
+ static QString className() { return "NodesShiftVisitor"; }
+
+ NodesShiftVisitor();
+ NodesShiftVisitor(Meters north_shift, Meters east_shift);
+ ~NodesShiftVisitor() override = default;
+
+ void setShift(Meters north_shift, Meters east_shift);
+
+ /**
+ * @see ElementVisitor
+ */
+ void visit(const std::shared_ptr& e) override;
+
+ QString getDescription() const override { return "Shift all nodes by an offset"; }
+ QString getName() const override { return className(); }
+ QString getClassName() const override { return className(); }
+
+private:
+
+ void _parseShift();
+
+ Meters _northShift;
+ Meters _eastShift;
+};
+
+}
+
+#endif // NODES_SHIFT_VISITOR_H
diff --git a/test-files/visitors/NodesShiftVisitorTest/applyToMapExpected1.osm b/test-files/visitors/NodesShiftVisitorTest/applyToMapExpected1.osm
new file mode 100644
index 0000000000..8a71050ff9
--- /dev/null
+++ b/test-files/visitors/NodesShiftVisitorTest/applyToMapExpected1.osm
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test-files/visitors/NodesShiftVisitorTest/applyToMapExpected2.osm b/test-files/visitors/NodesShiftVisitorTest/applyToMapExpected2.osm
new file mode 100644
index 0000000000..2c2c46f101
--- /dev/null
+++ b/test-files/visitors/NodesShiftVisitorTest/applyToMapExpected2.osm
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test-files/visitors/NodesShiftVisitorTest/applyToMapOutput2.osm b/test-files/visitors/NodesShiftVisitorTest/applyToMapOutput2.osm
new file mode 100644
index 0000000000..2c2c46f101
--- /dev/null
+++ b/test-files/visitors/NodesShiftVisitorTest/applyToMapOutput2.osm
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+