Index: /trunk/src/org/openstreetmap/josm/actions/JoinNodeWayAction.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/actions/JoinNodeWayAction.java	(revision 15609)
+++ /trunk/src/org/openstreetmap/josm/actions/JoinNodeWayAction.java	(revision 15610)
@@ -13,8 +13,11 @@
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
+import java.util.TreeMap;
 
 import javax.swing.JOptionPane;
@@ -91,5 +94,5 @@
         Collection<Node> selectedNodes = ds.getSelectedNodes();
         Collection<Command> cmds = new LinkedList<>();
-        Map<Way, MultiMap<Integer, Node>> data = new HashMap<>();
+        Map<Way, MultiMap<Integer, Node>> data = new LinkedHashMap<>();
 
         // If the user has selected some ways, only join the node to these.
@@ -100,5 +103,7 @@
         for (Node node : selectedNodes) {
             List<WaySegment> wss = mapView.getNearestWaySegments(mapView.getPoint(node), OsmPrimitive::isSelectable);
-            Set<Way> seenWays = new HashSet<>();
+            // we cannot trust the order of elements in wss because it was calculated based on rounded position value of node
+            TreeMap<Double, List<WaySegment>> nearestMap = new TreeMap<>();
+            EastNorth en = node.getEastNorth();
             for (WaySegment ws : wss) {
                 // Maybe cleaner to pass a "isSelected" predicate to getNearestWaySegments, but this is less invasive.
@@ -106,13 +111,38 @@
                     continue;
                 }
-                // only use the closest WaySegment of each way and ignore those that already contain the node
-                if (!ws.getFirstNode().equals(node) && !ws.getSecondNode().equals(node)
-                        && !seenWays.contains(ws.way)) {
-                    MultiMap<Integer, Node> innerMap = data.get(ws.way);
-                    if (innerMap == null) {
-                        innerMap = new MultiMap<>();
-                        data.put(ws.way, innerMap);
+                /* perpendicular distance squared
+                 * loose some precision to account for possible deviations in the calculation above
+                 * e.g. if identical (A and B) come about reversed in another way, values may differ
+                 * -- zero out least significant 32 dual digits of mantissa..
+                 */
+                double distSq = en.distanceSq(Geometry.closestPointToSegment(ws.getFirstNode().getEastNorth(),
+                        ws.getSecondNode().getEastNorth(), en));
+                // resolution in numbers with large exponent not needed here..
+                distSq = Double.longBitsToDouble(Double.doubleToLongBits(distSq) >> 32 << 32);
+                List<WaySegment> wslist = nearestMap.computeIfAbsent(distSq, k -> new LinkedList<>());
+                wslist.add(ws);
+            }
+            Set<Way> seenWays = new HashSet<>();
+            Double usedDist = null;
+            while (!nearestMap.isEmpty()) {
+                Entry<Double, List<WaySegment>> entry = nearestMap.pollFirstEntry();
+                if (usedDist != null) {
+                    double delta = entry.getKey() - usedDist;
+                    if (delta > 1e-4)
+                        break;
+                }
+                for (WaySegment ws : entry.getValue()) {
+                    // only use the closest WaySegment of each way and ignore those that already contain the node
+                    if (!ws.getFirstNode().equals(node) && !ws.getSecondNode().equals(node)
+                            && !seenWays.contains(ws.way)) {
+                        if (usedDist == null)
+                            usedDist = entry.getKey();
+                        MultiMap<Integer, Node> innerMap = data.get(ws.way);
+                        if (innerMap == null) {
+                            innerMap = new MultiMap<>();
+                            data.put(ws.way, innerMap);
+                        }
+                        innerMap.put(ws.lowerIndex, node);
                     }
-                    innerMap.put(ws.lowerIndex, node);
                     seenWays.add(ws.way);
                 }
@@ -142,4 +172,5 @@
                         if (prevMove != null) {
                             if (!prevMove.equalsEpsilon(newPosition, 1e-4)) {
+                                // very unlikely: node has same distance to multiple ways which are not nearly overlapping
                                 new Notification(tr("Multiple target ways, no common point found. Nothing was changed."))
                                         .setIcon(JOptionPane.INFORMATION_MESSAGE)
Index: /trunk/test/data/regress/18189/moveontocrossing.osm
===================================================================
--- /trunk/test/data/regress/18189/moveontocrossing.osm	(revision 15609)
+++ /trunk/test/data/regress/18189/moveontocrossing.osm	(revision 15610)
@@ -1,22 +1,22 @@
 <?xml version='1.0' encoding='UTF-8'?>
 <osm version='0.6' upload='never' generator='JOSM'>
-  <node id='-117814' action='modify' visible='true' lat='-21.08763547741' lon='-50.39117567184' />
-  <node id='-117816' action='modify' visible='true' lat='-21.09088329715' lon='-50.38820000246' />
-  <node id='-117818' action='modify' visible='true' lat='-21.09002420335' lon='-50.39142270855' />
-  <node id='-117820' action='modify' visible='true' lat='-21.08836886226' lon='-50.38800911046' />
-  <node id='-117822' action='modify' visible='true' lat='-21.089215371' lon='-50.38971309121'>
+  <node id='-117848' action='modify' lat='-21.08763547741' lon='-50.39117567184' />
+  <node id='-117850' action='modify' lat='-21.09088329715' lon='-50.38820000246' />
+  <node id='-117852' action='modify' lat='-21.09002420335' lon='-50.39142270855' />
+  <node id='-117854' action='modify' lat='-21.08836886226' lon='-50.38800911046' />
+  <node id='-117856' action='modify' lat='-21.08922243916' lon='-50.38958051884'>
     <tag k='name' v='select me and press N' />
   </node>
-  <node id='-117824' action='modify' visible='true' lat='-21.08920644697' lon='-50.38973634946' />
-  <way id='-117825' action='modify' visible='true'>
-    <nd ref='-117814' />
-    <nd ref='-117824' />
-    <nd ref='-117816' />
+  <node id='-117858' action='modify' lat='-21.08920644697' lon='-50.38973634946' />
+  <way id='-117859' action='modify'>
+    <nd ref='-117848' />
+    <nd ref='-117858' />
+    <nd ref='-117850' />
     <tag k='name' v='w1' />
   </way>
-  <way id='-117826' action='modify' visible='true'>
-    <nd ref='-117818' />
-    <nd ref='-117824' />
-    <nd ref='-117820' />
+  <way id='-117860' action='modify'>
+    <nd ref='-117852' />
+    <nd ref='-117858' />
+    <nd ref='-117854' />
     <tag k='name' v='w2' />
   </way>
Index: /trunk/test/data/regress/18420/user-sample.osm
===================================================================
--- /trunk/test/data/regress/18420/user-sample.osm	(revision 15610)
+++ /trunk/test/data/regress/18420/user-sample.osm	(revision 15610)
@@ -0,0 +1,119 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<osm version='0.6' generator='JOSM'>
+  <node id='-102242' action='modify' visible='true' lat='49.85445425656' lon='6.20573600175' />
+  <node id='-102249' action='modify' visible='true' lat='49.85339489918' lon='6.2054318408' />
+  <node id='-102257' action='modify' visible='true' lat='49.85474742627' lon='6.20620317373'>
+    <tag k='name' v='select me as 2nd' />
+  </node>
+  <node id='-102261' action='modify' visible='true' lat='49.8557970815' lon='6.2070500797' />
+  <node id='-102271' action='modify' visible='true' lat='49.85339561065' lon='6.2055471144' />
+  <node id='-102273' action='modify' visible='true' lat='49.85407425905' lon='6.20560045514' />
+  <node id='-102275' action='modify' visible='true' lat='49.85442524568' lon='6.2058417443' />
+  <node id='-102279' action='modify' visible='true' lat='49.85456260373' lon='6.20587782877' />
+  <node id='-102281' action='modify' visible='true' lat='49.85477467725' lon='6.20628677444' />
+  <node id='-102324' action='modify' visible='true' lat='49.85483992593' lon='6.20625346765' />
+  <node id='-102352' action='modify' visible='true' lat='49.85550656553' lon='6.20592355192' />
+  <node id='-102420' action='modify' visible='true' lat='49.85460321334' lon='6.20577606653' />
+  <node id='-102428' action='modify' visible='true' lat='49.85409117033' lon='6.20548808684' />
+  <node id='-102435' action='modify' visible='true' lat='49.85515876806' lon='6.20731720561' />
+  <node id='-102438' action='modify' visible='true' lat='49.85323533708' lon='6.20638577129' />
+  <node id='-102440' action='modify' visible='true' lat='49.85405635846' lon='6.20716871608' />
+  <node id='-102470' action='modify' visible='true' lat='49.85434275314' lon='6.20686769217' />
+  <node id='-102472' action='modify' visible='true' lat='49.85475017292' lon='6.20700602348' />
+  <node id='-102474' action='modify' visible='true' lat='49.8550229294' lon='6.20756865982' />
+  <node id='-102477' action='modify' visible='true' lat='49.85492640371' lon='6.20680415602' />
+  <node id='-102485' action='modify' visible='true' lat='49.85466388483' lon='6.20606218587'>
+    <tag k='name' v='select me 1st' />
+  </node>
+  <node id='-102529' action='modify' visible='true' lat='49.85342971443' lon='6.20722721195' />
+  <node id='-102543' action='modify' visible='true' lat='49.85556781365' lon='6.20791566341' />
+  <node id='-102545' action='modify' visible='true' lat='49.85591013282' lon='6.2080731523' />
+  <node id='-102548' action='modify' visible='true' lat='49.85319488861' lon='6.20547371315' />
+  <node id='-102550' action='modify' visible='true' lat='49.85297440265' lon='6.20532071944' />
+  <node id='-102557' action='modify' visible='true' lat='49.85287576256' lon='6.20505073848' />
+  <node id='-102559' action='modify' visible='true' lat='49.8528525531' lon='6.20471776196' />
+  <node id='-102561' action='modify' visible='true' lat='49.85293378617' lon='6.20388082098' />
+  <node id='-102708' action='modify' visible='true' lat='49.85575212081' lon='6.20732366502' />
+  <node id='-102763' action='modify' visible='true' lat='49.8547871834' lon='6.2060710761' />
+  <node id='-103160' action='modify' visible='true' lat='49.85430297902' lon='6.20577066869' />
+  <node id='-103218' action='modify' visible='true' lat='49.85420527205' lon='6.20566069946' />
+  <node id='-103257' action='modify' visible='true' lat='49.85470936517' lon='6.20589941472' />
+  <node id='-103485' action='modify' visible='true' lat='49.85424936952' lon='6.20556682214' />
+  <node id='-103491' action='modify' visible='true' lat='49.85435312899' lon='6.20567947358' />
+  <node id='-103499' action='modify' visible='true' lat='49.85577978893' lon='6.20668529928' />
+  <node id='-103514' action='modify' visible='true' lat='49.85560686117' lon='6.20779037207' />
+  <node id='-103563' action='modify' visible='true' lat='49.8559423371' lon='6.20794325799' />
+  <node id='-103800' action='modify' visible='true' lat='49.85460214873' lon='6.20684891671' />
+  <node id='-104035' action='modify' visible='true' lat='49.85539243377' lon='6.20589672983' />
+  <way id='-104036' action='modify' visible='true'>
+    <nd ref='-102242' />
+    <nd ref='-103491' />
+    <nd ref='-103485' />
+    <nd ref='-102428' />
+    <nd ref='-102249' />
+    <nd ref='-102271' />
+    <nd ref='-102273' />
+    <nd ref='-103218' />
+    <nd ref='-103160' />
+    <nd ref='-102275' />
+    <nd ref='-102279' />
+    <nd ref='-102257' />
+    <nd ref='-102477' />
+    <nd ref='-102435' />
+    <nd ref='-102543' />
+    <nd ref='-102545' />
+    <nd ref='-103563' />
+    <nd ref='-103514' />
+    <nd ref='-102708' />
+    <nd ref='-102261' />
+    <nd ref='-103499' />
+    <nd ref='-102352' />
+    <nd ref='-104035' />
+    <nd ref='-102324' />
+    <nd ref='-102763' />
+    <nd ref='-103257' />
+    <nd ref='-102420' />
+    <nd ref='-102242' />
+    <tag k='landuse' v='grass' />
+  </way>
+  <way id='-104037' action='modify' visible='true'>
+    <nd ref='-102561' />
+    <nd ref='-102559' />
+    <nd ref='-102557' />
+    <nd ref='-102550' />
+    <nd ref='-102548' />
+    <nd ref='-102271' />
+    <nd ref='-102273' />
+    <nd ref='-103218' />
+    <nd ref='-103160' />
+    <nd ref='-102275' />
+    <nd ref='-102279' />
+    <nd ref='-102281' />
+    <nd ref='-102477' />
+    <nd ref='-102435' />
+    <nd ref='-102543' />
+    <nd ref='-102545' />
+    <tag k='highway' v='unclassified' />
+  </way>
+  <way id='-104038' action='modify' visible='true'>
+    <nd ref='-102273' />
+    <nd ref='-102271' />
+    <nd ref='-102438' />
+    <nd ref='-102529' />
+    <nd ref='-102440' />
+    <nd ref='-102470' />
+    <nd ref='-103800' />
+    <nd ref='-102472' />
+    <nd ref='-102474' />
+    <nd ref='-102435' />
+    <nd ref='-102477' />
+    <nd ref='-102281' />
+    <nd ref='-102485' />
+    <nd ref='-102279' />
+    <nd ref='-102275' />
+    <nd ref='-103160' />
+    <nd ref='-103218' />
+    <nd ref='-102273' />
+    <tag k='landuse' v='forest' />
+  </way>
+</osm>
Index: /trunk/test/unit/org/openstreetmap/josm/actions/JoinNodeWayActionTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/actions/JoinNodeWayActionTest.java	(revision 15609)
+++ /trunk/test/unit/org/openstreetmap/josm/actions/JoinNodeWayActionTest.java	(revision 15610)
@@ -6,8 +6,8 @@
 import java.awt.Rectangle;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
@@ -37,9 +37,9 @@
     @Rule
     @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().projection().main().preferences().projection();
+    public JOSMTestRules test = new JOSMTestRules().projection().main().preferences();
 
     private void setupMapView(DataSet ds) {
-        // setup a reasonable screen size
-        MainApplication.getMap().mapView.setBounds(new Rectangle(1920, 1080));
+        // setup a reasonable size for the edit window
+        MainApplication.getMap().mapView.setBounds(new Rectangle(1345, 939));
         if (ds.getDataSourceBoundingBox() != null) {
             MainApplication.getMap().mapView.zoomTo(ds.getDataSourceBoundingBox());
@@ -59,5 +59,4 @@
      */
     @Test
-    @Ignore
     public void testTicket18189() throws Exception {
         DataSet dataSet = new DataSet();
@@ -105,5 +104,4 @@
      */
     @Test
-    @Ignore
     public void testTicket11508() throws Exception {
         DataSet ds = OsmReader.parseDataSet(TestUtils.getRegressionDataStream(11508, "11508_example.osm"), null);
@@ -134,5 +132,4 @@
      */
     @Test
-    @Ignore
     public void testTicket18189Crossing() throws Exception {
         DataSet ds = OsmReader.parseDataSet(TestUtils.getRegressionDataStream(18189, "moveontocrossing.osm"), null);
@@ -160,5 +157,4 @@
      */
     @Test
-    @Ignore
     public void testTicket18189ThreeWays() throws Exception {
         DataSet ds = OsmReader.parseDataSet(TestUtils.getRegressionDataStream(18189, "data.osm"), null);
@@ -184,3 +180,36 @@
     }
 
+    /**
+     * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/18420">Bug #18420</a>.
+     * @throws Exception if an error occurs
+     */
+    @Test
+    public void testTicket18420() throws Exception {
+        DataSet ds = OsmReader.parseDataSet(TestUtils.getRegressionDataStream(18420, "user-sample.osm"), null);
+        Layer layer = new OsmDataLayer(ds, OsmDataLayer.createNewName(), null);
+        MainApplication.getLayerManager().addLayer(layer);
+        try {
+            List<Node> nodesToMove = ds.getNodes().stream().filter(n -> n.hasTag("name")).collect(Collectors.toList());
+            assertTrue(nodesToMove.size() == 2);
+            Node n = nodesToMove.iterator().next();
+            if (!n.hasTag("name", "select me 1st"))
+                Collections.reverse(nodesToMove);
+            Node toMove1 = nodesToMove.get(0);
+            Node toMove2 = nodesToMove.get(1);
+            Node expected1 = new Node(new LatLon(49.8546658263727, 6.206059532463773));
+            Node expected2 = new Node(new LatLon(49.854738602108085, 6.206213646054511));
+            ds.setSelected(nodesToMove);
+            setupMapView(ds);
+            JoinNodeWayAction action = JoinNodeWayAction.createMoveNodeOntoWayAction();
+            action.setEnabled(true);
+            action.actionPerformed(null);
+            assertTrue("Node was moved to an unexpected position", toMove1.getEastNorth().equalsEpsilon(expected1.getEastNorth(), 1e-7));
+            assertTrue("Node was moved to an unexpected position", toMove2.getEastNorth().equalsEpsilon(expected2.getEastNorth(), 1e-7));
+            assertTrue("Node was not added to expected number of ways", toMove1.getParentWays().size() == 2);
+            assertTrue("Node was not added to expected number of ways", toMove2.getParentWays().size() == 2);
+        } finally {
+            MainApplication.getLayerManager().removeLayer(layer);
+        }
+    }
+
 }
