Task: Bug/github5613 (rdkit/rdkit#5735)

org: rdkit
repo: rdkit
pr_number: 5735
issue_title: Bug/github5613
issue_body: <!--
Thanks for contributing a pull request! 
-->
#### Reference Issue

Fixes #5613 

#### What does this implement/fix? Explain your changes.

Copies the target molecule onto the core template preserving stereochemistry, aromaticity and explicit hydrogens.

New routine is in `RCore::extractCoreFromMolMatch`.  I also expanded the *allowMultipleRGroupsOnUnlabelled* option so that
there is an explicit bond to an attachment point for all free valence in the core template.  This necessitated changes to  
`RCore::matchTerminalUserRGroups` to better handle matching.  There are a bunch of other changes as the matching core for each target now has an explicit hydrogen model.

I've put the test results in [this gist](https://gist.github.com/jones-gareth/40383fc76313e0365d4058e48d6f2193)

#### Any other comments?

There is one wierdness- one of the expected smiles has changed for `allowMultipleRGroupsOnUnlabelled` from `C1SC2CC([*:1])NC(N([*:2])[*:4])C2C1[*:6]` to `C1C2SCC([*:6])C2C(N([*:2])[*:4])NC1[*:1]`. But this is the same structure:

```
mol1 = Chem.MolFromSmiles('C1SC2CC([*:1])NC(N([*:2])[*:4])C2C1[*:6]')
# new expected
mol2 = Chem.MolFromSmiles('C1C2SCC([*:6])C2C(N([*:2])[*:4])NC1[*:1]')
Draw.MolsToGridImage([mol1, mol2], 2)
```
![image](https://user-images.githubusercontent.com/24233741/200425499-ce02ceb4-ff53-48e7-8ac2-2c85b634b334.png)

I'm wondering if I'm not updating properties, re-initializing after editing molecules or have otherwise created a corrupt structure.  I anybody can point me in the right direction that would be great.
resolved_issues: number: 5613
title: RGD Stereochemistry in decomposed structure is not copied to the matching core
body: **Describe the bug**

Given a core query that does not specify stereochemistry and a matching structure with stereochemistry the stereochmistry is not copied to the matching core output in the decomposition.

**To Reproduce**

```python
# %%
from rdkit import Chem
from rdkit.Chem import AllChem
from rdkit.Chem.rdRGroupDecomposition import RGroupDecomposition, RGroupDecompositionParameters, \
    RGroupMatching, RGroupScore, RGroupLabels, RGroupCoreAlignment, RGroupLabelling
from rdkit.Chem.Draw import IPythonConsole
IPythonConsole.ipython_useSVG = True
from rdkit.Chem import PandasTools
from rdkit.Chem import Draw
from IPython.display import HTML
import pandas as pd

# %%
core = Chem.MolFromSmarts('[*:1]C(=O)NC1CCN([*:3])C1')
core

# %%
mol = Chem.MolFromSmiles('Cc1c(c(c([nH]1)C(=O)N[C@H]2CCN(C2)c3cc(nc(n3)Cl)C(=O)O)Cl)Cl')
mol

# %%
options = RGroupDecompositionParameters()
decomp = RGroupDecomposition([core], options)
decomp.Add(mol)
decomp.Process()
groups = decomp.GetRGroupsAsColumns()
df = PandasTools.RGroupDecompositionToFrame(groups, [mol], include_core=True)
PandasTools.ChangeMoleculeRendering(df)
HTML(df.to_html())

```

**Expected behavior**

I expect to see the chiral center in the molecule appear in the matching core.

**Screenshots**

The core query does not have a chiral center specified:

![image](https://user-images.githubusercontent.com/24233741/193469019-6646cdd7-70b0-4a9b-baa4-c93eb8336972.png)

The molecule does on the Pyrrolidine N:

![image](https://user-images.githubusercontent.com/24233741/197821924-98bae79c-7992-43f0-a784-7cb99f5a8e16.png)

but the matching core does not contain the chiral center:

![image](https://user-images.githubusercontent.com/24233741/193469237-346c0fef-a808-431d-a88d-4d83cc909c82.png)

**Configuration (please complete the following information):**

 - RDKit version: master as of 9/2022
 - OS: [e.g. Ubuntu 20.04] Ubuntu 20.04
 - Python version (if relevant):
 - Are you using conda?
 - If you are using conda, which channel did you install the rdkit from?
 - If you are not using conda: how did you install the RDKit?

**Additional context**

While bond and atom information is copied from the input core to the output/matching core, stereochemistry is not.

I can take a look, but if anyone could point me to existing code that does something similar that would be helpful.  I guess the issue would be that the parity/atom ordering round the chiral center in the input core and matching core could be different.


Reference solution:
fix_patch: diff --git a/Code/GraphMol/ChemTransforms/ChemTransforms.cpp b/Code/GraphMol/ChemTransforms/ChemTransforms.cpp
index afb958a3034..38d9882712c 100644
--- a/Code/GraphMol/ChemTransforms/ChemTransforms.cpp
+++ b/Code/GraphMol/ChemTransforms/ChemTransforms.cpp
@@ -32,7 +32,7 @@
 #include <RDGeneral/BadFileException.h>
 #include <GraphMol/ChemTransforms/ChemTransforms.h>
 namespace RDKit {
-namespace {
+namespace details {
 void updateSubMolConfs(const ROMol &mol, RWMol &res,
                        boost::dynamic_bitset<> &removedAtoms) {
   // update conformer information:
@@ -52,7 +52,9 @@ void updateSubMolConfs(const ROMol &mol, RWMol &res,
     res.addConformer(newConf, false);
   }
 }
+}  // namespace details
 
+namespace {
 struct SideChainMapping {
   int molIndex;
   int coreIndex;
@@ -136,7 +138,7 @@ ROMol *deleteSubstructs(const ROMol &mol, const ROMol &query, bool onlyFrags,
   res->commitBatchEdit();
   // if we removed any atoms, clear the computed properties:
   if (delList.size()) {
-    updateSubMolConfs(mol, *res, removedAtoms);
+    details::updateSubMolConfs(mol, *res, removedAtoms);
 
     res->clearComputedProps(true);
     // update our properties, but allow unhappiness:
@@ -242,7 +244,7 @@ std::vector<ROMOL_SPTR> replaceSubstructs(
     // clear conformers and computed props and do basic updates
     // on the resulting molecules, but allow unhappiness:
     for (auto &re : res) {
-      updateSubMolConfs(mol, *(RWMol *)re.get(), removedAtoms);
+      details::updateSubMolConfs(mol, *(RWMol *)re.get(), removedAtoms);
       re->clearComputedProps(true);
       re->updatePropertyCache(false);
     }
@@ -327,7 +329,7 @@ ROMol *replaceSidechains(const ROMol &mol, const ROMol &coreQuery,
   }
   newMol->commitBatchEdit();
 
-  updateSubMolConfs(mol, *newMol, removedAtoms);
+  details::updateSubMolConfs(mol, *newMol, removedAtoms);
 
   // clear computed props and do basic updates on the
   // the resulting molecule, but allow unhappiness:
@@ -628,6 +630,10 @@ ROMol *replaceCore(const ROMol &mol, const ROMol &core,
             if (switchIt) {
               sidechainAtom->invertChirality();
             }
+            // the neighbor count needs to be decremented, in case an additional
+            // bond to the core is present at this chiral center (unlikely!),
+            // because this bond will now be at the end of the neighbor list.
+            --whichNbr;
           }
         }
         ++whichNbr;
@@ -676,7 +682,7 @@ ROMol *replaceCore(const ROMol &mol, const ROMol &core,
   }
   newMol->commitBatchEdit();
 
-  updateSubMolConfs(mol, *newMol, removedAtoms);
+  details::updateSubMolConfs(mol, *newMol, removedAtoms);
 
   // Update any terminal dummy atom coordinates after removing atoms not in the
   // keeplist and calling updateSubMolConfs
@@ -795,7 +801,7 @@ ROMol *MurckoDecompose(const ROMol &mol) {
   }
   res->commitBatchEdit();
 
-  updateSubMolConfs(mol, *res, removedAtoms);
+  details::updateSubMolConfs(mol, *res, removedAtoms);
   res->clearComputedProps();
 
   return (ROMol *)res;
diff --git a/Code/GraphMol/ChemTransforms/ChemTransforms.h b/Code/GraphMol/ChemTransforms/ChemTransforms.h
index 0d248b82f2a..978aeb64cfb 100644
--- a/Code/GraphMol/ChemTransforms/ChemTransforms.h
+++ b/Code/GraphMol/ChemTransforms/ChemTransforms.h
@@ -265,6 +265,12 @@ RDKIT_CHEMTRANSFORMS_EXPORT void parseQueryDefText(
     std::map<std::string, ROMOL_SPTR> &queryDefs, bool standardize = true,
     const std::string &delimiter = "\t", const std::string &comment = "//",
     unsigned int nameColumn = 0, unsigned int smartsColumn = 1);
-}  // namespace RDKit
 
+namespace details {
+//! not recommended for use in other code
+RDKIT_CHEMTRANSFORMS_EXPORT void updateSubMolConfs(
+    const ROMol &mol, RWMol &res, boost::dynamic_bitset<> &removedAtoms);
+}  // namespace details
+
+}  // namespace RDKit
 #endif
diff --git a/Code/GraphMol/RGroupDecomposition/RGroupCore.cpp b/Code/GraphMol/RGroupDecomposition/RGroupCore.cpp
index 6732d4668f2..9017134d865 100644
--- a/Code/GraphMol/RGroupDecomposition/RGroupCore.cpp
+++ b/Code/GraphMol/RGroupDecomposition/RGroupCore.cpp
@@ -9,7 +9,9 @@
 //  of the RDKit source tree.
 //
 #include "RGroupCore.h"
-#include <GraphMol/Substruct/SubstructUtils.h>
+#include "GraphMol/SmilesParse/SmilesWrite.h"
+#include "GraphMol/ChemTransforms/ChemTransforms.h"
+#include "GraphMol/Substruct/SubstructUtils.h"
 
 namespace RDKit {
 
@@ -17,17 +19,25 @@ namespace {
 // From answer 12 in
 // https://stackoverflow.com/questions/5279051/how-can-i-create-cartesian-product-of-vector-of-vectors
 // by anumi
+// modified to exclude duplicates
 static std::vector<std::vector<int>> cartesianProduct(
-    const std::vector<std::vector<int>> &v) {
+    const std::vector<std::vector<int>> &v, bool allowDuplicates) {
   std::vector<std::vector<int>> s = {{}};
   for (const auto &u : v) {
     std::vector<std::vector<int>> r;
     for (const auto &x : s) {
       for (const auto y : u) {
-        r.push_back(x);
-        r.back().push_back(y);
+        // check for duplicates
+        if (allowDuplicates || std::find(x.begin(), x.end(), y) == x.end()) {
+          r.push_back(x);
+          r.back().push_back(y);
+        }
       }
     }
+    if (r.empty()) {
+      // unable to extend
+      return r;
+    }
     s = std::move(r);
   }
   return s;
@@ -54,6 +64,223 @@ void RCore::findIndicesWithRLabel() {
   }
 }
 
+std::pair<RWMOL_SPTR, bool> RCore::extractCoreFromMolMatch(
+    const ROMol &mol, const MatchVectType &match,
+    const RGroupDecompositionParameters &params) const {
+  auto extractedCore = boost::make_shared<RWMol>(mol);
+  std::set<int> atomIndicesToKeep;
+  std::vector<Bond *> newBonds;
+  std::map<Atom *, int> dummyAtomMap;
+  bool hasCoreDummies = false;
+  for (auto &pair : match) {
+    const auto queryAtom = core->getAtomWithIdx(pair.first);
+    auto const targetAtom = extractedCore->getAtomWithIdx(pair.second);
+    if (int rLabel; queryAtom->getPropIfPresent(RLABEL, rLabel)) {
+      targetAtom->setProp(RLABEL, rLabel);
+    }
+    if (int rLabelType; queryAtom->getPropIfPresent(RLABEL_TYPE, rLabelType)) {
+      targetAtom->setProp(RLABEL_TYPE, rLabelType);
+    }
+
+    if (!hasCoreDummies && queryAtom->getAtomicNum() == 0) {
+      hasCoreDummies = true;
+    }
+    if (queryAtom->getAtomicNum() == 0 && queryAtom->hasProp(RLABEL) &&
+        queryAtom->getDegree() == 1) {
+      continue;
+    } else {
+      atomIndicesToKeep.insert(pair.second);
+      int neighborNumber = -1;
+#ifdef VERBOSE
+      std::cerr << "Atom Chirality In " << targetAtom->getChiralTag()
+                << std::endl;
+#endif
+      bool isChiral = targetAtom->getChiralTag() == Atom::CHI_TETRAHEDRAL_CCW ||
+                      targetAtom->getChiralTag() == Atom::CHI_TETRAHEDRAL_CW;
+      if (isChiral && !params.substructmatchParams.useChirality) {
+        // if we're not doing chiral matching don't copy chirality to the
+        // extracted core.
+        targetAtom->setChiralTag(Atom::CHI_UNSPECIFIED);
+        isChiral = false;
+      }
+      // collect neighbors in vector, so we can add atoms while looping
+      std::vector<Atom *> targetNeighborAtoms;
+      for (auto targetNeighborAtom : extractedCore->atomNeighbors(targetAtom)) {
+        targetNeighborAtoms.push_back(targetNeighborAtom);
+      }
+      // explicit hydrogens to keep in extracted core to preserve chiralty
+      std::vector<int> hydrogensToAdd;
+      for (const auto targetNeighborAtom : targetNeighborAtoms) {
+        ++neighborNumber;
+        auto targetNeighborIndex = targetNeighborAtom->getIdx();
+        auto queryNeighborMapping = std::find_if(
+            match.begin(), match.end(),
+            [this, targetNeighborIndex, pair](const auto &p) {
+              return p.second == static_cast<int>(targetNeighborIndex) &&
+                     core->getBondBetweenAtoms(pair.first, p.first);
+            });
+        if (queryNeighborMapping == match.end()) {
+          if (targetNeighborAtom->getAtomicNum() == 1) {
+            // Hydrogen needed to define chirality is present in target but
+            // not mapped to core.  Copy it to the extracted core
+            hydrogensToAdd.push_back(static_cast<int>(targetNeighborIndex));
+          } else if (isChiral) {
+            // There is a heavy sidechain in the decomp that is connected to
+            // the core by an unknown bond (onlyMatchAtRGroups = False and
+            // allowMultipleRGroupsOnUnlabelled = False).
+            // As there is no explicit target bond chriality is not preserved.
+            // In some cases we could handle chirality, but that has not been
+            // implemented (if there is only one free bond on the query that
+            // would be easy- or we could arbitrarily assign bonds).
+            isChiral = false;
+            targetAtom->setChiralTag(Atom::CHI_UNSPECIFIED);
+          }
+          continue;
+        }
+        const auto queryNeighbor =
+            core->getAtomWithIdx((*queryNeighborMapping).first);
+        if (queryNeighbor->getAtomicNum() == 0 &&
+            queryNeighbor->hasProp(RLABEL) && queryNeighbor->getDegree() == 1) {
+          auto newDummy = new Atom(*queryNeighbor);
+          dummyAtomMap[newDummy] = static_cast<int>(targetNeighborIndex);
+          newDummy->clearComputedProps();
+          const auto newDummyIdx =
+              extractedCore->addAtom(newDummy, false, true);
+          atomIndicesToKeep.insert(static_cast<int>(newDummyIdx));
+          auto connectingBond =
+              extractedCore
+                  ->getBondBetweenAtoms(pair.second, targetNeighborIndex)
+                  ->copy();
+          if (connectingBond->getStereo() > Bond::BondStereo::STEREOANY) {
+            // TODO: how to handle bond stereo on rgroups connected to core by
+            // stereo double bonds
+            connectingBond->setStereo(Bond::BondStereo::STEREOANY);
+          }
+
+          if (connectingBond->getBeginAtomIdx() == targetNeighborIndex) {
+            connectingBond->setBeginAtomIdx(newDummyIdx);
+          } else {
+            connectingBond->setEndAtomIdx(newDummyIdx);
+          }
+          newBonds.push_back(connectingBond);
+
+          // Chirality parity stuff see RDKit::replaceCore in
+          // Code/GraphMol/ChemTransforms/ChemTransforms.cpp
+          if (isChiral) {
+            bool switchIt = false;
+            switch (extractedCore->getAtomDegree(targetAtom)) {
+              case 4:
+                if (!(neighborNumber % 2)) {
+                  switchIt = true;
+                }
+                break;
+              case 3:
+                if (neighborNumber == 1) {
+                  switchIt = true;
+                }
+                break;
+            }
+            // because this neighbor will be moved at the end of the neighbor
+            // list, we need to decrement the neighbor number in case additional
+            // dummy atoms are to be connected to the targetAtom
+            --neighborNumber;
+            if (switchIt) {
+              targetAtom->invertChirality();
+            }
+          }
+        }
+      }
+      for (const int index : hydrogensToAdd) {
+        atomIndicesToKeep.insert(index);
+      }
+#ifdef VERBOSE
+      std::cerr << "Atom Chirality Out " << targetAtom->getChiralTag()
+                << std::endl;
+#endif
+    }
+  }
+  for (const auto newBond : newBonds) {
+    extractedCore->addBond(newBond, true);
+  }
+
+  // Now delete atom's that are not in the core.
+  extractedCore->beginBatchEdit();
+  boost::dynamic_bitset<> removedAtoms(mol.getNumAtoms());
+  for (const auto atom : extractedCore->atoms()) {
+    if (atomIndicesToKeep.find(atom->getIdx()) == atomIndicesToKeep.end()) {
+      extractedCore->removeAtom(atom);
+      removedAtoms.set(atom->getIdx());
+    }
+  }
+  extractedCore->commitBatchEdit();
+
+  // Copy molecule coordinates to extracted core
+  details::updateSubMolConfs(mol, *extractedCore, removedAtoms);
+
+  for (auto citer = mol.beginConformers(); citer != mol.endConformers();
+       ++citer) {
+    Conformer &newConf =
+        extractedCore->getConformer(static_cast<int>((*citer)->getId()));
+    for (const auto &[fst, snd] : dummyAtomMap) {
+      newConf.setAtomPos(fst->getIdx(), (*citer)->getAtomPos(snd));
+    }
+  }
+
+  // If the molecule has no coordinates and the core does, copy those over
+  if (!mol.getNumConformers() && core->getNumConformers()) {
+    ROMol molCopy(mol);
+    for (auto citer = core->beginConformers(); citer != core->endConformers();
+         ++citer) {
+      const auto newConf = new Conformer(mol.getNumAtoms());
+      newConf->setId((*citer)->getId());
+      newConf->set3D((*citer)->is3D());
+      for (const auto &[fst, snd] : match) {
+        newConf->setAtomPos(snd, (*citer)->getAtomPos(fst));
+      }
+      molCopy.addConformer(newConf);
+    }
+    details::updateSubMolConfs(molCopy, *extractedCore, removedAtoms);
+    molCopy.clearConformers();
+
+    for (const auto atom : extractedCore->atoms()) {
+      if (isUserRLabel(*atom)) {
+        int rLabel = atom->getProp<int>(RLABEL);
+        for (const auto coreAtom : core->atoms()) {
+          if (int l; coreAtom->getPropIfPresent(RLABEL, l) && l == rLabel) {
+            int i = 0;
+            for (auto citer = core->beginConformers();
+                 citer != core->endConformers(); ++citer, ++i) {
+              extractedCore->getConformer(i).setAtomPos(
+                  atom->getIdx(), (*citer)->getAtomPos(coreAtom->getIdx()));
+            }
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  extractedCore->clearComputedProps(true);
+  extractedCore->updatePropertyCache(false);
+
+#ifdef VERBOSE
+  std::cerr << "Extracted core smiles " << MolToSmiles(*extractedCore)
+            << std::endl;
+  std::cerr << "Extracted core smarts " << MolToSmarts(*extractedCore)
+            << std::endl;
+#endif
+
+  try {
+    unsigned int failed;
+    MolOps::sanitizeMol(*extractedCore, failed,
+                        MolOps::SANITIZE_SYMMRINGS | MolOps::SANITIZE_CLEANUP);
+  } catch (const MolSanitizeException &) {
+  }
+
+  std::pair rtn(extractedCore, hasCoreDummies);
+  return rtn;
+}
+
 // Return a copy of core where dummy atoms are replaced by
 // the respective matching atom in mol, while other atoms have
 // their aromatic flag and formal charge copied from
@@ -73,23 +300,6 @@ ROMOL_SPTR RCore::replaceCoreAtomsWithMolMatches(
     }
   }
 
-  // look for dummy rgroup attachment points in core that are not in mapping
-  // there is no r group for these guys.  I've marked them as missing.
-  // It might be better to delete them coreReplaceAtoms, but I'm not sure of the
-  // implications of that.
-  for (auto atom : coreReplacedAtoms->atoms()) {
-    if (atom->getAtomicNum() == 0 && atom->hasProp(RLABEL)) {
-      auto missing = std::find_if(match.cbegin(), match.cend(),
-                                  [atom](const std::pair<int, int> &p) {
-                                    return static_cast<unsigned int>(p.first) ==
-                                           atom->getIdx();
-                                  }) == match.end();
-      if (missing) {
-        atom->setProp<bool>(MISSING_RGROUP, true);
-      }
-    }
-  }
-
   std::map<int, int> matchLookup(match.cbegin(), match.cend());
   for (auto bond : coreReplacedAtoms->bonds()) {
     if (bond->hasQuery()) {
@@ -170,68 +380,6 @@ void RCore::replaceCoreAtom(RWMol &mol, Atom &atom, const Atom &other) const {
   }
 }
 
-// Final core returned to user with dummy atoms and bonds set to those in the
-// match
-RWMOL_SPTR RCore::coreWithMatches(const ROMol &coreReplacedAtoms) const {
-  auto finalCore = boost::make_shared<RWMol>(*labelledCore);
-  for (size_t atomIdx = 0; atomIdx < coreReplacedAtoms.getNumAtoms();
-       ++atomIdx) {
-    auto coreAtom = finalCore->getAtomWithIdx(atomIdx);
-    auto templateAtom = coreReplacedAtoms.getAtomWithIdx(atomIdx);
-    auto unlabelledCoreAtom = core->getAtomWithIdx(atomIdx);
-    if (templateAtom->getAtomicNum() > 0 &&
-        isAtomWithMultipleNeighborsOrNotDummyRGroupAttachment(
-            *unlabelledCoreAtom)) {
-      replaceCoreAtom(*finalCore, *coreAtom, *templateAtom);
-    }
-  }
-
-  for (size_t bondIdx = 0; bondIdx < coreReplacedAtoms.getNumBonds();
-       ++bondIdx) {
-    auto coreBond = finalCore->getBondWithIdx(bondIdx);
-    if (coreBond->hasQuery()) {
-      auto templateBond = coreReplacedAtoms.getBondWithIdx(bondIdx);
-      Bond newBond(templateBond->getBondType());
-      newBond.setIsAromatic(templateBond->getIsAromatic());
-      finalCore->replaceBond(bondIdx, &newBond, true);
-    }
-  }
-
-  // update core coordinates to input structures
-  if (coreReplacedAtoms.getNumConformers() && finalCore->getNumConformers()) {
-    const auto &replacedConformer = coreReplacedAtoms.getConformer();
-    auto &finalConformer = finalCore->getConformer();
-
-    size_t atomIdx = 0;
-    for (; atomIdx < coreReplacedAtoms.getNumAtoms(); ++atomIdx) {
-      auto molPoint = replacedConformer.getAtomPos(atomIdx);
-      finalConformer.setAtomPos(atomIdx, molPoint);
-    }
-    // Dummy atom coordinates are not in input structure, so calculate them here
-    for (; atomIdx < finalCore->getNumAtoms(); atomIdx++) {
-      const auto atom = finalCore->getAtomWithIdx(atomIdx);
-      const int neighborIdx = *finalCore->getAtomNeighbors(atom).first;
-      MolOps::setTerminalAtomCoords(*finalCore, atomIdx, neighborIdx);
-    }
-  }
-
-  // Remove unmapped dummies
-  std::vector<Atom *> atomsToRemove;
-  for (const auto atom : coreReplacedAtoms.atoms()) {
-    if (atom->getAtomicNum() == 0 && atom->hasProp(MISSING_RGROUP)) {
-      atomsToRemove.push_back(finalCore->getAtomWithIdx(atom->getIdx()));
-    }
-  }
-  finalCore->beginBatchEdit();
-  for (auto atom : atomsToRemove) {
-    finalCore->removeAtom(atom);
-  }
-  finalCore->commitBatchEdit();
-
-  finalCore->updatePropertyCache(false);
-  return finalCore;
-}
-
 // matching the core to the target molecule is a two step process
 // First match to a reduced representation (the core minus terminal user
 // R-groups). Next, match the R-groups. We do this as the core may not be a
@@ -251,6 +399,8 @@ void RCore::buildMatchingMol() {
   for (auto atom : matchingMol->atoms()) {
     // keep track of the original core index in the matching molecule atom
     atom->setProp<int>(RLABEL_CORE_INDEX, atom->getIdx());
+    // TODO for unlabelled core attachments if the heavy neighbor is not dummy
+    // then keep one attachment
     if (atom->getAtomicNum() == 0 && atom->getDegree() == 1 &&
         isDummyRGroupAttachment(*atom)) {
       // remove terminal user R groups and save core atom index and mapping to
@@ -302,48 +452,163 @@ std::vector<MatchVectType> RCore::matchTerminalUserRGroups(
   // target atoms that the R group can map to
   std::map<int, std::vector<int>> availableMappingsForDummyMap;
 
-  // Loop over all the user terminal R groups and see if they can be included
-  // in the match and, if so, record the target atoms that the R group can
-  // be mapped to.
-  for (const auto dummyIdx : terminalRGroupDummyAtoms) {
-    // find the heavy atom in the core attached to the dummy
-    const int neighborIdx = terminalRGroupAtomToNeighbor.find(dummyIdx)->second;
-    const auto coreBond = core->getBondBetweenAtoms(dummyIdx, neighborIdx);
-    // find the atom in the target mapped to the neighbor in the core
-    const int targetIdx = matchMap[neighborIdx];
-    const auto targetAtom = target.getAtomWithIdx(targetIdx);
-    ROMol::ADJ_ITER nbrIter, endNbrs;
-    std::vector<int> available;
-    // now look for neighbors of that target atom that are not mapped to a core
-    // atom- the dummy atom can potentially be mapped to each of those
-    boost::tie(nbrIter, endNbrs) = target.getAtomNeighbors(targetAtom);
-    while (nbrIter != endNbrs) {
-      if (mappedTargetIdx.find(*nbrIter) == mappedTargetIdx.end()) {
-        const auto targetBond = target.getBondBetweenAtoms(targetIdx, *nbrIter);
-        // check for bond compatibility
-        if (bondCompat(coreBond, targetBond, sssParams)) {
-          available.push_back(*nbrIter);
+  std::set<int> symmetricHydrogens;
+
+  // keep a count of target atoms and the number of connections to the core
+  std::map<int, int> targetAtomBondsToCoreCounts;
+  for (const auto coreAtom : core->atoms()) {
+    if (coreAtom->getAtomicNum() == 1) {
+      continue;
+    }
+    if (terminalRGroupDummyAtoms.find(coreAtom->getIdx()) !=
+        terminalRGroupDummyAtoms.end()) {
+      continue;
+    }
+    std::vector<int> dummyIndexes;
+    for (auto neighbor : core->atomNeighbors(coreAtom)) {
+      if (terminalRGroupDummyAtoms.find(neighbor->getIdx()) !=
+          terminalRGroupDummyAtoms.end()) {
+        dummyIndexes.push_back(neighbor->getIdx());
+      }
+    }
+    if (dummyIndexes.empty()) {
+      continue;
+    }
+
+    // Sort dummies based on user RLABEL (ascending) then unlabeled
+    // (descending as they are negative)
+    std::sort(dummyIndexes.begin(), dummyIndexes.end(), [this](int a, int b) {
+      auto dummy = core->getAtomWithIdx(a);
+      auto otherDummy = core->getAtomWithIdx(b);
+      auto l1 = dummy->getProp<int>(RLABEL);
+      auto l2 = otherDummy->getProp<int>(RLABEL);
+      return l1 > 0 && l2 > 0 ? l1 < l2 : l1 > l2;
+    });
+
+    // Find what target atoms will bond to these dummies
+    std::vector<std::vector<int>> neighborDummyLists;
+    for (auto dummyIndex : dummyIndexes) {
+      const int neighborIdx =
+          terminalRGroupAtomToNeighbor.find(dummyIndex)->second;
+      const auto coreBond = core->getBondBetweenAtoms(dummyIndex, neighborIdx);
+      // find the atom in the target mapped to the neighbor in the core
+      const int targetIdx = matchMap[neighborIdx];
+      const auto targetAtom = target.getAtomWithIdx(targetIdx);
+      ROMol::ADJ_ITER nbrIter, endNbrs;
+      std::vector<int> available;
+      // now look for neighbors of that target atom that are not mapped to a
+      // core atom- the dummy atom can potentially be mapped to each of those
+      boost::tie(nbrIter, endNbrs) = target.getAtomNeighbors(targetAtom);
+      while (nbrIter != endNbrs) {
+        if (mappedTargetIdx.find(*nbrIter) == mappedTargetIdx.end()) {
+          const auto targetBond =
+              target.getBondBetweenAtoms(targetIdx, *nbrIter);
+          // check for bond compatibility
+          if (bondCompat(coreBond, targetBond, sssParams)) {
+            available.push_back(*nbrIter);
+            targetAtomBondsToCoreCounts[*nbrIter]++;
+          }
+        }
+        ++nbrIter;
+      }
+
+      if (available.size() > 1) {
+        bool allHydrogens = std::all_of(
+            available.begin(), available.end(), [&target](const int idx) {
+              return target.getAtomWithIdx(idx)->getAtomicNum() == 1;
+            });
+        if (allHydrogens) {
+          // If all neighbors are hydrogens we don't need to iterate through
+          // them- just assign the first free hydrogen. Could extend to cover
+          // symmetric groups in general
+          auto hydrogen = std::find_if(available.begin(), available.end(),
+                                       [&symmetricHydrogens](const int idx) {
+                                         return symmetricHydrogens.find(idx) ==
+                                                symmetricHydrogens.end();
+                                       });
+          int singleHydrogen =
+              hydrogen == available.end() ? *available.begin() : *hydrogen;
+          std::vector<int> hydrogenVec{singleHydrogen};
+          available = hydrogenVec;
+          symmetricHydrogens.insert(singleHydrogen);
         }
       }
-      ++nbrIter;
+      neighborDummyLists.push_back(available);
     }
 
-    if (available.size()) {
-      availableMappingsForDummyMap[dummyIdx] = available;
-    } else {
-      // We can't map this dummy to a target atom.  That is OK if it is an
-      // unlabelled core attachment atom or a user R label connected to a
-      // query or wildcard atom
-      const auto dummy = core->getAtomWithIdx(dummyIdx);
-      const auto neighbor = core->getAtomWithIdx(neighborIdx);
-      if (dummy->hasProp(UNLABELLED_CORE_ATTACHMENT)) {
-        missingDummies.push_back(dummyIdx);
-      } else if (isUserRLabel(*dummy) &&
-                 (neighbor->getAtomicNum() == 0 || neighbor->hasQuery())) {
-        // https://github.com/rdkit/rdkit/issues/4505
-        missingDummies.push_back(dummyIdx);
+    // Now search through all the dummies and see if we need to exclude any as
+    // it may not be possible to assign target atoms to all R groups.  If that
+    // is the case exclude R Groups with higher labels
+
+    // use negative numbers to indicate a group is excluded
+    std::vector<std::vector<int>> neighborListsWithUnmapped(
+        neighborDummyLists.size());
+    int start = 0;
+    std::transform(neighborDummyLists.begin(), neighborDummyLists.end(),
+                   neighborListsWithUnmapped.begin(),
+                   [&start](std::vector<int> v) {
+                     v.push_back(--start);
+                     return v;
+                   });
+    // could optimize this product as depth search to return as soon as we
+    // have a permutation of all positive numbers
+    auto cp = cartesianProduct(neighborListsWithUnmapped, false);
+    // now sort
+    bool foundAll = false;
+    std::sort(cp.begin(), cp.end(),
+              [&foundAll](const std::vector<int> a, std::vector<int> b) {
+                auto isNegative = [](int v) -> bool { return v < 0; };
+                // firstly to minimize number of unmapped dummies
+                const int numUnmappedA =
+                    std::count_if(a.begin(), a.end(), isNegative);
+                const int numUnmappedB =
+                    std::count_if(b.begin(), b.end(), isNegative);
+                if (numUnmappedA < numUnmappedB) {
+                  return true;
+                }
+                if (numUnmappedB == 0 || numUnmappedA == 0) {
+                  foundAll = true;
+                }
+                if (!foundAll && numUnmappedA == numUnmappedB && numUnmappedA) {
+                  // in a tie prefer the permutation that excludes the r group
+                  // with the lowest label
+                  // don't need to sort these if we've found an all mapped
+                  // combination
+                  for (size_t i = 0; i < a.size(); ++i) {
+                    const int v1 = a[i];
+                    const int v2 = b[i];
+                    if ((v1 > 0 && v2 > 0) || (v1 < 0 && v2 < 0)) {
+                      continue;
+                    }
+                    if (v1 > 0 && v2 < 0) {
+                      return true;
+                    }
+                    if (v1 < 0 && v2 > 0) {
+                      break;
+                    }
+                  }
+                }
+                return false;
+              });
+    auto best = cp[0];
+    for (size_t i = 0; i < dummyIndexes.size(); i++) {
+      auto dummyIdx = dummyIndexes[i];
+      if (best[i] < 0) {
+        // We can't map this dummy to a target atom.  That is OK if it is an
+        // unlabeled core attachment atom or a user R label connected to a
+        // query or wildcard atom
+        const auto dummy = core->getAtomWithIdx(dummyIdx);
+        if (dummy->hasProp(UNLABELED_CORE_ATTACHMENT)) {
+          missingDummies.push_back(dummyIdx);
+        } else if (isUserRLabel(*dummy) &&
+                   (coreAtom->getAtomicNum() == 0 || coreAtom->hasQuery())) {
+          // https://github.com/rdkit/rdkit/issues/4505
+          missingDummies.push_back(dummyIdx);
+        } else {
+          return allMappings;
+        }
       } else {
-        return allMappings;
+        availableMappingsForDummyMap[dummyIdx] = neighborDummyLists[i];
       }
     }
   }
@@ -352,51 +617,6 @@ std::vector<MatchVectType> RCore::matchTerminalUserRGroups(
     return allMappings;
   }
 
-  // find singleton duplicate mappings. This can happen if there are 2 user
-  // specified R groups attached to a query atom and only one of the R groups
-  // can match (see second case in https://github.com/rdkit/rdkit/issues/4505)
-  // We remove the R group with the highest label
-  for (auto mapping = availableMappingsForDummyMap.begin();
-       mapping != availableMappingsForDummyMap.end();) {
-    auto values = mapping->second;
-    bool erased = false;
-    if (values.size() == 1) {
-      const auto dummy = core->getAtomWithIdx(mapping->first);
-      auto [nbrIter, endNbrs] = core->getAtomNeighbors(dummy);
-      auto heavyNeighbor = core->getAtomWithIdx(*nbrIter);
-      bool userQueryDummy =
-          isUserRLabel(*dummy) &&
-          (heavyNeighbor->getAtomicNum() == 0 || heavyNeighbor->hasQuery());
-      if (userQueryDummy) {
-        boost::tie(nbrIter, endNbrs) = core->getAtomNeighbors(heavyNeighbor);
-        while (nbrIter != endNbrs) {
-          int otherIdx = *nbrIter;
-          if (otherIdx != mapping->first) {
-            auto mappingIter = availableMappingsForDummyMap.find(otherIdx);
-            if (mappingIter != availableMappingsForDummyMap.end() &&
-                mappingIter->second.size() == 1 &&
-                mappingIter->second[0] == values[0]) {
-              auto otherDummy = core->getAtomWithIdx(otherIdx);
-              bool remove = isUserRLabel(*otherDummy) &&
-                            dummy->getProp<int>(RLABEL) >
-                                otherDummy->getProp<int>(RLABEL);
-              if (remove) {
-                missingDummies.push_back(mapping->first);
-                availableMappingsForDummyMap.erase(mapping++);
-                erased = true;
-                break;
-              }
-            }
-          }
-          nbrIter++;
-        }
-      }
-    }
-    if (!erased) {
-      ++mapping;
-    }
-  }
-
   std::vector<int> dummiesWithMapping;
   std::vector<std::vector<int>> availableMappingsForDummy;
   for (const auto &mapping : availableMappingsForDummyMap) {
@@ -405,10 +625,19 @@ std::vector<MatchVectType> RCore::matchTerminalUserRGroups(
   }
 
   // enumerate over all available atoms using a cartesian product.
-  // this is not the most ideal way to do things as the straightforward product
-  // will allow duplicates.  In the unlikely evert that there is a performance
-  // issue a custom enumerator will be needed
-  const auto allAvailableMappings = cartesianProduct(availableMappingsForDummy);
+  // only allow duplicates if a target atom can be bonded to more than one query
+  // atom
+  const bool allowDuplicates =
+      std::find_if(targetAtomBondsToCoreCounts.begin(),
+                   targetAtomBondsToCoreCounts.end(),
+                   [](const std::pair<int, int> &p) { return p.second > 1; }) !=
+      targetAtomBondsToCoreCounts.end();
+  const auto allAvailableMappings =
+      cartesianProduct(availableMappingsForDummy, allowDuplicates);
+  if (allAvailableMappings.empty()) {
+    allMappings.push_back(match);
+    return allMappings;
+  }
   // the size of the final mapping
   size_t size = allAvailableMappings[0].size() + match.size();
   // these indices are needed for the whole molecule match check functor
@@ -418,7 +647,7 @@ std::vector<MatchVectType> RCore::matchTerminalUserRGroups(
   const std::string indexProp("__core_index__");
   bool hasMissing = !missingDummies.empty();
   if (hasMissing) {
-    // if there are dummies that we can map these need to be removed from the
+    // if there are dummies that we can't map these need to be removed from the
     // query before atom-by-atom matching.  Create a copy of the query for that
     // and use properties to map atoms back to the core
     for (auto atom : core->atoms()) {
@@ -458,8 +687,8 @@ std::vector<MatchVectType> RCore::matchTerminalUserRGroups(
                                             sssParams);
   boost::dynamic_bitset<> targetBondsPresent(target.getNumBonds());
 
-  // Filter all available mappings removing those that contain duplicates,
-  // or violate chirality
+  // Filter all available mappings removing those that violate chirality or have
+  // duplicate bonds
   for (const auto &dummyMapping : allAvailableMappings) {
     CHECK_INVARIANT(match.size() + dummyMapping.size() == size,
                     "Size error in dummy mapping");
@@ -471,20 +700,22 @@ std::vector<MatchVectType> RCore::matchTerminalUserRGroups(
                                    : dummiesWithMapping[i];
       queryIndices[position] = queryIndex;
       targetIndices[position] = dummyMapping[i];
-      const int neighborIdx =
-          terminalRGroupAtomToNeighbor.find(dummiesWithMapping[i])->second;
-      const int targetNeighborIdx = matchMap[neighborIdx];
-      const auto targetBond =
-          target.getBondBetweenAtoms(dummyMapping[i], targetNeighborIdx);
-      CHECK_INVARIANT(targetBond != nullptr, "Matching target bond not found");
-      const auto targetBondIdx = targetBond->getIdx();
-      // check for duplicates- some of this could also be handled within a
-      // modified cartesian product.
-      if (targetBondsPresent[targetBondIdx]) {
-        duplicateBonds = true;
-        break;
+      if (allowDuplicates) {
+        const int neighborIdx =
+            terminalRGroupAtomToNeighbor.find(dummiesWithMapping[i])->second;
+        const int targetNeighborIdx = matchMap[neighborIdx];
+        const auto targetBond =
+            target.getBondBetweenAtoms(dummyMapping[i], targetNeighborIdx);
+        CHECK_INVARIANT(targetBond != nullptr,
+                        "Matching target bond not found");
+        const auto targetBondIdx = targetBond->getIdx();
+        // check for duplicates
+        if (targetBondsPresent[targetBondIdx]) {
+          duplicateBonds = true;
+          break;
+        }
+        targetBondsPresent[targetBondIdx] = 1;
       }
-      targetBondsPresent[targetBondIdx] = 1;
     }
     // use MolMatchFinalCheckFunctor to check this match works with chirality
     if (!duplicateBonds && molMatchFunctor(queryIndices, targetIndices)) {
diff --git a/Code/GraphMol/RGroupDecomposition/RGroupCore.h b/Code/GraphMol/RGroupDecomposition/RGroupCore.h
index 9a493a8ab29..92a551ca9c5 100644
--- a/Code/GraphMol/RGroupDecomposition/RGroupCore.h
+++ b/Code/GraphMol/RGroupDecomposition/RGroupCore.h
@@ -50,15 +50,17 @@ struct RCore {
 
   // Return a copy of core where dummy atoms are replaced by
   // the respective matching atom in mol, while other atoms have
-  // their aromatic flag and formal charge copied from 
+  // their aromatic flag and formal charge copied from
   // the respective matching atom in mol
   ROMOL_SPTR replaceCoreAtomsWithMolMatches(bool &hasCoreDummies,
                                             const ROMol &mol,
                                             const MatchVectType &match) const;
 
-  // Final core returned to user with dummy atoms and bonds set to those in the
-  // match
-  RWMOL_SPTR coreWithMatches(const ROMol &coreReplacedAtoms) const;
+  // Final core returned to user, created by extracting core from target
+  // molecule
+  std::pair<RWMOL_SPTR, bool> extractCoreFromMolMatch(
+      const ROMol &mol, const MatchVectType &match,
+      const RGroupDecompositionParameters &params) const;
 
   std::vector<MatchVectType> matchTerminalUserRGroups(
       const RWMol &target, MatchVectType match,
@@ -80,7 +82,8 @@ struct RCore {
  private:
   // The set of atom indices in the core for terminal R groups with user label
   std::set<int> terminalRGroupDummyAtoms;
-  // The set of atom indices in the core for terminal R groups with atom indices with or without user labels
+  // The set of atom indices in the core for terminal R groups with atom indices
+  // with or without user labels
   std::set<int> terminalRGroupAtoms;
   // An atom index map of terminal R groups to their heavy atom neighbor
   std::map<int, int> terminalRGroupAtomToNeighbor;
@@ -92,8 +95,8 @@ struct RCore {
 
   // Build the matching molecule (core minus user R groups)
   void buildMatchingMol();
-  
-  // Add attachement points to unlabelled R Groups
+
+  // Add attachment points to unlabelled R Groups
   void addDummyAtomsToUnlabelledCoreAtoms();
 };
 
diff --git a/Code/GraphMol/RGroupDecomposition/RGroupDecomp.cpp b/Code/GraphMol/RGroupDecomposition/RGroupDecomp.cpp
index b8a2c72cec9..52ca467a963 100644
--- a/Code/GraphMol/RGroupDecomposition/RGroupDecomp.cpp
+++ b/Code/GraphMol/RGroupDecomposition/RGroupDecomp.cpp
@@ -59,8 +59,7 @@ const std::string done = "RLABEL_PROCESSED";
 const std::string CORE = "Core";
 const std::string RPREFIX = "R";
 const std::string _rgroupInputDummy = "_rgroupInputDummy";
-const std::string UNLABELLED_CORE_ATTACHMENT = "unlabeledCoreAttachment";
-const std::string MISSING_RGROUP = "missingRGroup";
+const std::string UNLABELED_CORE_ATTACHMENT = "unlabeledCoreAttachment";
 
 namespace {
 void ADD_MATCH(R_DECOMP &match, int rlabel) {
@@ -123,7 +122,7 @@ int RGroupDecomposition::add(const ROMol &inmol) {
       // a substructure match for the molecule if a single molecule atom matches
       // 2 RGroup attachments (see https://github.com/rdkit/rdkit/pull/4002)
 
-      // match the reduced represenation:
+      // match the reduced representation:
       std::vector<MatchVectType> baseMatches =
           SubstructMatch(mol, *core.second.matchingMol, sssparams);
       tmatches.clear();
@@ -258,6 +257,7 @@ int RGroupDecomposition::add(const ROMol &inmol) {
     const bool labelByIndex = true;
     const bool requireDummyMatch = false;
     bool hasCoreDummies = false;
+    // TODO see if we need relaceCoreWithMolMatches or can just use rcore->core
     auto coreCopy =
         rcore->replaceCoreAtomsWithMolMatches(hasCoreDummies, mol, tmatche);
     tMol.reset(replaceCore(mol, *coreCopy, tmatche, replaceDummies,
@@ -302,6 +302,15 @@ int RGroupDecomposition::add(const ROMol &inmol) {
 
                 data->labels.insert(rlabel);  // keep track of all labels used
                 attachments.push_back(rlabel);
+                if (const auto [bondIdx, end] = newMol->getAtomBonds(at); bondIdx != end) {
+                  auto connectingBond = (*newMol)[*bondIdx];
+                  if (connectingBond->getStereo() > Bond::BondStereo::STEREOANY) {
+                    // TODO: how to handle bond stereo on rgroups connected to core by
+                    // stereo double bonds
+                    connectingBond->setStereo(Bond::BondStereo::STEREOANY);
+                  }
+                }
+
               }
             } else {
               // restore input wildcard
@@ -377,10 +386,11 @@ int RGroupDecomposition::add(const ROMol &inmol) {
             rcore->numberUserRGroups - numberUserGroupsInMatch;
         CHECK_INVARIANT(numberMissingUserGroups >= 0,
                         "Data error in missing user rgroup count");
-        potentialMatches.emplace_back(
-            core_idx, numberMissingUserGroups, match,
-            hasCoreDummies || !data->params.onlyMatchAtRGroups ? coreCopy
-                                                               : nullptr);
+        const auto [extractedCore, hasDummies] =
+            rcore->extractCoreFromMolMatch(mol, tmatche, params());
+        hasCoreDummies = hasDummies;
+        potentialMatches.emplace_back(core_idx, numberMissingUserGroups, match,
+                                      extractedCore);
       }
     }
   }
@@ -444,19 +454,26 @@ std::vector<std::string> RGroupDecomposition::getRGroupLabels() const {
 
 RWMOL_SPTR RGroupDecomposition::outputCoreMolecule(
     const RGroupMatch &match, const UsedLabelMap &usedLabelMap) const {
+  // this routine could probably be merged into RGroupDecompData::relabelCore
+
   const auto &core = data->cores[match.core_idx];
   if (!match.matchedCore) {
     return core.labelledCore;
   }
-  auto coreWithMatches = core.coreWithMatches(*match.matchedCore);
+  auto coreWithMatches = match.matchedCore;
+#ifdef VERBOSE
+  std::cerr << "output core mol1 " << MolToSmarts(*coreWithMatches)
+            << std::endl;
+#endif
+  std::map<Atom *, int> retainedRGroups;
   for (auto atomIdx = coreWithMatches->getNumAtoms(); atomIdx--;) {
     auto atom = coreWithMatches->getAtomWithIdx(atomIdx);
     if (atom->getAtomicNum()) {
       continue;
     }
     auto label = data->getRlabel(atom);
-    auto missingRGroup = atom->hasProp(MISSING_RGROUP);
-    bool atomRemoved = false;
+    // Always convert to hydrogen - then remove later if
+    // removeHydrogensPostMatch is set
     Atom *nbrAtom = nullptr;
     for (const auto &nbri :
          boost::make_iterator_range(coreWithMatches->getAtomNeighbors(atom))) {
@@ -464,38 +481,64 @@ RWMOL_SPTR RGroupDecomposition::outputCoreMolecule(
       break;
     }
     if (nbrAtom) {
-      bool isUserDefinedLabel =
+      const bool isUserDefinedLabel =
           usedLabelMap.has(label) && usedLabelMap.isUserDefined(label);
-      auto numExplicitHs = nbrAtom->getNumExplicitHs();
-      if (usedLabelMap.has(label) && usedLabelMap.getIsUsed(label)) {
-        if (numExplicitHs) {
-          nbrAtom->setNumExplicitHs(numExplicitHs - 1);
-        }
-      } else if (!isUserDefinedLabel ||
-                 data->params.removeAllHydrogenRGroupsAndLabels) {
-        coreWithMatches->removeAtom(atomIdx);
-        atomRemoved = true;
-        // if we remove an unused label from an aromatic atom,
-        // we need to check whether we need to adjust its explicit
-        // H count, or it will fail to kekulize
-        if (isUserDefinedLabel && nbrAtom->getIsAromatic() && !missingRGroup) {
-          nbrAtom->updatePropertyCache(false);
-          if (!numExplicitHs) {
-            nbrAtom->setNumExplicitHs(nbrAtom->getExplicitValence() -
-                                      nbrAtom->getDegree());
-          }
-        }
+      const bool isUsedLabel =
+          usedLabelMap.has(label) && usedLabelMap.getIsUsed(label);
+      if (!isUsedLabel && (!isUserDefinedLabel ||
+                           data->params.removeAllHydrogenRGroupsAndLabels)) {
+        // Always convert to hydrogen - then remove later if
+        // removeHydrogensPostMatch is set
+        atom->setAtomicNum(1);
+        atom->updatePropertyCache(false);
+      } else {
+        retainedRGroups[atom] = label;
       }
-      nbrAtom->updatePropertyCache(false);
-      if (!atomRemoved && coreWithMatches->getNumConformers() > 0) {
-        auto iter = match.rgroups.find(label);
-        if (iter != match.rgroups.end() && iter->second->is_hydrogen) {
-          MolOps::setTerminalAtomCoords(*coreWithMatches, atomIdx,
-                                        nbrAtom->getIdx());
+    }
+  }
+
+#ifdef VERBOSE
+  std::cerr << "output core mol2 " << MolToSmiles(*coreWithMatches)
+            << std::endl;
+#endif
+  if (data->params.removeHydrogensPostMatch) {
+    RDLog::LogStateSetter blocker;
+    constexpr bool implicitOnly = false;
+    constexpr bool updateExplicitCount = false;
+    constexpr bool sanitize = false;
+    MolOps::removeHs(*coreWithMatches, implicitOnly, updateExplicitCount,
+                     sanitize);
+    coreWithMatches->updatePropertyCache(false);
+  }
+
+  if (coreWithMatches->getNumConformers() > 0) {
+    for (const auto &[atom, label] : retainedRGroups) {
+      if (usedLabelMap.has(label) && usedLabelMap.isUserDefined(label)) {
+        // coordinates of user defined R groups should already be copied over
+        continue;
+      }
+      const auto neighbor = *coreWithMatches->atomNeighbors(atom).begin();
+      const auto &mapping = data->finalRlabelMapping;
+      if (const auto oldLabel = std::find_if(
+              mapping.begin(), mapping.end(),
+              [label = label](const auto &p) { return p.second == label; });
+          oldLabel != mapping.end()) {
+        if (auto iter = match.rgroups.find(oldLabel->first);
+            iter != match.rgroups.end()) {
+          MolOps::setTerminalAtomCoords(*coreWithMatches, atom->getIdx(),
+                                        neighbor->getIdx());
         }
       }
     }
   }
+
+  if (!coreWithMatches->getRingInfo()->isInitialized()) {
+    MolOps::symmetrizeSSSR(*coreWithMatches);
+  }
+#ifdef VERBOSE
+  std::cerr << "output core mol3 " << MolToSmiles(*coreWithMatches)
+            << std::endl;
+#endif
   return coreWithMatches;
 }
 
diff --git a/Code/GraphMol/RGroupDecomposition/RGroupDecompData.h b/Code/GraphMol/RGroupDecomposition/RGroupDecompData.h
index 3130576592f..83f3caa08b3 100644
--- a/Code/GraphMol/RGroupDecomposition/RGroupDecompData.h
+++ b/Code/GraphMol/RGroupDecomposition/RGroupDecompData.h
@@ -219,6 +219,7 @@ struct RGroupDecompData {
       }
     }
 
+    std::set<int> labelsToErase;
     for (int label : labels) {
       if (label > 0 && !removeAllHydrogenRGroups) {
         continue;
@@ -236,11 +237,35 @@ struct RGroupDecompData {
       }
 
       if (allH) {
+        labelsToErase.insert(label);
         for (auto &position : results) {
           position.rgroups.erase(label);
         }
       }
     }
+
+    for (auto &position : results) {
+      for (auto atom : position.matchedCore->atoms()) {
+        if (int atomLabel; atom->getAtomicNum() == 0 &&
+                           atom->getPropIfPresent(RLABEL, atomLabel)) {
+          if (atomLabel > 0 && !params.removeAllHydrogenRGroupsAndLabels) {
+            continue;
+          }
+          if (labelsToErase.find(atomLabel) != labelsToErase.end()) {
+            atom->setAtomicNum(1);
+            atom->clearProp(RLABEL);
+            if (atom->hasProp(RLABEL_TYPE)) {
+              atom->clearProp(RLABEL_TYPE);
+            }
+            if (atom->hasProp(UNLABELED_CORE_ATTACHMENT)) {
+              atom->clearProp(UNLABELED_CORE_ATTACHMENT);
+            }
+            atom->updatePropertyCache(false);
+          }
+        }
+      }
+    }
+
     return results;
   }
 
@@ -286,9 +311,31 @@ struct RGroupDecompData {
     }
   }
 
+  bool replaceHydrogenCoreDummy(const RGroupMatch &match, RWMol &core,
+                                const Atom &atom, const int currentLabel,
+                                const int rLabel) {
+    // if the R group is just a hydrogen then the attachment point should
+    // replace an existing hydrogen neighbor since all hydrogen neighbors
+    // are copied from the input molecule to the extracted core.
+    if (const auto group = match.rgroups.find(currentLabel);
+        group != match.rgroups.end()) {
+      if (group->second->is_hydrogen) {
+        for (auto &neighbor : core.atomNeighbors(&atom)) {
+          if (neighbor->getAtomicNum() == 1) {
+            neighbor->setAtomicNum(0);
+            setRlabel(neighbor, rLabel);
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
   void relabelCore(RWMol &core, std::map<int, int> &mappings,
                    UsedLabels &used_labels, const std::set<int> &indexLabels,
-                   const std::map<int, std::vector<int>> &extraAtomRLabels) {
+                   const std::map<int, std::vector<int>> &extraAtomRLabels,
+                   const RGroupMatch *const match = nullptr) {
     // Now remap to proper rlabel ids
     //  if labels are positive, they come from User labels
     //  if they are negative, they come from indices and should be
@@ -304,7 +351,6 @@ struct RGroupDecompData {
     // a sidechain atom has a vector of the attachments back to the
     //  core that takes the place of numBondsToRlabel
 
-    std::map<int, std::vector<int>> bondsToCore;
     std::vector<std::pair<Atom *, Atom *>> atomsToAdd;  // adds -R if necessary
 
     // Deal with user supplied labels
@@ -320,10 +366,26 @@ struct RGroupDecompData {
       if (atom->getAtomicNum() == 0 &&
           atom->getDegree() == 1) {  // add to existing dummy/rlabel
         setRlabel(atom, userLabel);
-      } else {  // adds new rlabel
-        auto *newAt = new Atom(0);
-        setRlabel(newAt, userLabel);
-        atomsToAdd.emplace_back(atom, newAt);
+      } else {
+        // A non-terminal RGroup. Create a dummy by replacing an existing
+        // hydrogen for hydrogen side chains, or a new dummy atom for heavy side
+        // chains.
+        bool addNew = true;
+        if (match != nullptr) {
+          addNew = !replaceHydrogenCoreDummy(*match, core, *atom, userLabel,
+                                             userLabel);
+          // If we can't replace a hydrogen only add the dummy if it exists in
+          // the decomp This is unexpected.
+          if (addNew &&
+              match->rgroups.find(userLabel) == match->rgroups.end()) {
+            addNew = false;
+          }
+        }
+        if (addNew) {
+          auto *newAt = new Atom(0);
+          setRlabel(newAt, userLabel);
+          atomsToAdd.emplace_back(atom, newAt);
+        }
       }
     }
 
@@ -350,9 +412,21 @@ struct RGroupDecompData {
               *atom)) {  // add to dummy
         setRlabel(atom, rlabel);
       } else {
-        auto *newAt = new Atom(0);
-        setRlabel(newAt, rlabel);
-        atomsToAdd.emplace_back(atom, newAt);
+        bool addNew = true;
+        if (match != nullptr) {
+          addNew =
+              !replaceHydrogenCoreDummy(*match, core, *atom, newLabel, rlabel);
+          // If we can't replace a hydrogen only add the dummy if it exists in
+          // the decomp This is unexpected.
+          if (addNew && match->rgroups.find(newLabel) == match->rgroups.end()) {
+            addNew = false;
+          }
+        }
+        if (addNew) {
+          auto *newAt = new Atom(0);
+          setRlabel(newAt, rlabel);
+          atomsToAdd.emplace_back(atom, newAt);
+        }
       }
     }
 
@@ -382,6 +456,11 @@ struct RGroupDecompData {
       atom->clearProp(RLABEL);
       atom->clearProp(RLABEL_TYPE);
     }
+
+    // Delay removing hydrogens from core until outputCoreMolecule is called,
+    // If hydrogens are removed now and more dummies removed in
+    // outputCoreMolecule then aromaticity perception in the core may be broken.
+
     core.updatePropertyCache(false);  // this was github #1550
   }
 
@@ -532,6 +611,16 @@ struct RGroupDecompData {
       for (auto &rgroup : it.rgroups) {
         relabelRGroup(*rgroup.second, finalRlabelMapping);
       }
+#ifdef VERBOSE
+      std::cerr << "relabel core mol1 " << MolToSmiles(*it.matchedCore)
+                << std::endl;
+#endif
+      relabelCore(*it.matchedCore, finalRlabelMapping, used_labels, indexLabels,
+                  extraAtomRLabels, &it);
+#ifdef VERBOSE
+      std::cerr << "relabel core mol2 " << MolToSmiles(*it.matchedCore)
+                << std::endl;
+#endif
     }
 
     std::set<int> uniqueMappedValues;
diff --git a/Code/GraphMol/RGroupDecomposition/RGroupDecompParams.cpp b/Code/GraphMol/RGroupDecomposition/RGroupDecompParams.cpp
index af3b94d8dde..3086e68c3fc 100644
--- a/Code/GraphMol/RGroupDecomposition/RGroupDecompParams.cpp
+++ b/Code/GraphMol/RGroupDecomposition/RGroupDecompParams.cpp
@@ -14,6 +14,7 @@
 #include <GraphMol/Substruct/SubstructMatch.h>
 #include <GraphMol/SmilesParse/SmilesParse.h>
 #include <GraphMol/FMCS/FMCS.h>
+#include <GraphMol/QueryBond.h>
 #include <set>
 
 namespace RDKit {
@@ -155,7 +156,8 @@ bool RGroupDecompositionParameters::prepareCore(RWMol &core,
   }
 
   int maxLabel = 1;
-  if (alignCore && (alignment & MCS)) {
+  // makes no sense to do MCS alignment if we are only matching at user defined R-Groups
+  if (alignCore && !onlyMatchAtRGroups && (alignment & MCS)) {
     std::vector<ROMOL_SPTR> mols;
     mols.push_back(ROMOL_SPTR(new ROMol(core)));
     mols.push_back(ROMOL_SPTR(new ROMol(*alignCore)));
@@ -250,7 +252,7 @@ bool RGroupDecompositionParameters::prepareCore(RWMol &core,
       }
 
       if (!found && (autoLabels & DummyAtomLabels) && atom->getDegree() == 1 &&
-          !atom->hasProp(UNLABELLED_CORE_ATTACHMENT)) {
+          !atom->hasProp(UNLABELED_CORE_ATTACHMENT)) {
         const bool forceRelabellingWithDummies = true;
         int defaultDummyStartLabel = maxLabel;
         if (setLabel(atom, defaultDummyStartLabel, foundLabels, maxLabel,
@@ -265,6 +267,9 @@ bool RGroupDecompositionParameters::prepareCore(RWMol &core,
     //  we have used (note that these are negative since they are
     //  potential rgroups and haven't been assigned yet)
     if (!found && (autoLabels & AtomIndexLabels)) {
+      // we should not need these on (non r group) core atoms when
+      // allowMultipleRGroupsOnUnlabelled is set, but it is useful in case
+      // insufficient dummy groups are added to the core
       if (setLabel(atom, indexOffset - atom->getIdx(), foundLabels, maxLabel,
                    relabel, Labelling::INDEX_LABELS)) {
         nextOffset++;
@@ -310,50 +315,64 @@ void RGroupDecompositionParameters::addDummyAtomsToUnlabelledCoreAtoms(
     return;
   }
 
-  // add a single rgroup substitution to any atom with free valence that doesn't
-  // have a terminal dummy neighbor
-
-  std::vector<Atom *> unlabelledCoreAtoms{};
+  // add R group substitutions to fill atomic valence
+  std::vector<Atom *> unlabeledCoreAtoms{};
   for (const auto atom : core.atoms()) {
     if (atom->getAtomicNum() == 1) {
       continue;
     }
-
     if (hasLabel(atom, labels)) {
       continue;
     }
+    unlabeledCoreAtoms.push_back(atom);
+  }
 
-    if (atom->getAtomicNum() > 0 && atom->calcImplicitValence() <= 1 &&
-        !atom->hasQuery()) {
-      continue;
-    }
-
-    auto [nbrIdx, endNbrs] = core.getAtomNeighbors(atom);
-    bool hasTerminalDummyNeighbor = false;
+  for (const auto atom : unlabeledCoreAtoms) {
+    atom->calcImplicitValence(false);
+    const auto atomIndex = atom->getIdx();
+    int dummiesToAdd;
+    int maxNumDummies = 4 - static_cast<int>(atom->getDegree());
 
-    while (nbrIdx != endNbrs) {
-      const auto neighbor = core.getAtomWithIdx(*nbrIdx);
-      if (neighbor->getAtomicNum() == 0 && neighbor->getDegree() == 1) {
-        hasTerminalDummyNeighbor = true;
-        break;
+    // figure out the number of dummies to add
+    if (atom->getAtomicNum() == 0) {
+      dummiesToAdd = maxNumDummies;
+    } else {
+      double bondOrder = 0;
+      for (const auto bond : core.atomBonds(atom)) {
+        auto contrib = bond->getValenceContrib(atom);
+        if (contrib == 0.0 && bond->hasQuery()) {
+          contrib = 1.0;
+        }
+        bondOrder += contrib;
       }
-      ++nbrIdx;
-    }
-    if (!hasTerminalDummyNeighbor) {
-      unlabelledCoreAtoms.push_back(atom);
+
+      const auto &valances =
+          PeriodicTable::getTable()->getValenceList(atom->getAtomicNum());
+      auto valence = *std::max_element(valances.begin(), valances.end());
+      // round up aromatic contributions
+      dummiesToAdd = valence - (int)(bondOrder + .51);
+      dummiesToAdd = std::min(dummiesToAdd,maxNumDummies);
     }
-  }
 
-  for (const auto atom : unlabelledCoreAtoms) {
-    auto atomIndex = atom->getIdx();
-    auto newAtom = new Atom(0);
-    newAtom->setProp<bool>(UNLABELLED_CORE_ATTACHMENT, true);
-    auto newIdx = core.addAtom(newAtom, false, true);
-    core.addBond(atomIndex, newIdx, Bond::SINGLE);
-    auto dummy = core.getAtomWithIdx(newIdx);
-    dummy->updatePropertyCache();
-    if (core.getNumConformers() > 0) {
-      MolOps::setTerminalAtomCoords(core, newIdx, atomIndex);
+    std::vector<int> newIndices;
+    for (int i = 0; i < dummiesToAdd; i++) {
+      const auto newAtom = new Atom(0);
+      newAtom->setProp<bool>(UNLABELED_CORE_ATTACHMENT, true);
+      const auto newIdx = core.addAtom(newAtom, false, true);
+      newIndices.push_back(newIdx);
+      auto *qb = new QueryBond();
+      qb->setQuery(makeBondNullQuery());
+      qb->setBeginAtomIdx(atomIndex);
+      qb->setEndAtomIdx(newIdx);
+      core.addBond(qb, true);
+      const auto dummy = core.getAtomWithIdx(newIdx);
+      dummy->updatePropertyCache();
+    }
+    atom->updatePropertyCache(false);
+    for (const auto newIdx : newIndices) {
+      if (core.getNumConformers() > 0) {
+        MolOps::setTerminalAtomCoords(core, newIdx, atomIndex);
+      }
     }
   }
 }
diff --git a/Code/GraphMol/RGroupDecomposition/RGroupMatch.h b/Code/GraphMol/RGroupDecomposition/RGroupMatch.h
index 2a1cbc70f09..a3041e8e2a3 100644
--- a/Code/GraphMol/RGroupDecomposition/RGroupMatch.h
+++ b/Code/GraphMol/RGroupDecomposition/RGroupMatch.h
@@ -20,10 +20,10 @@ struct RGroupMatch {
   size_t core_idx;  // index of the matching core
   size_t numberMissingUserRGroups;
   R_DECOMP rgroups;        // rlabel->RGroupData mapping
-  ROMOL_SPTR matchedCore;  // Core with dummy or query atoms and bonds matched
+  RWMOL_SPTR matchedCore;  // Core with dummy or query atoms and bonds matched
 
   RGroupMatch(size_t core_index, size_t numberMissingUserRGroups,
-              R_DECOMP input_rgroups, ROMOL_SPTR matchedCore)
+              R_DECOMP input_rgroups, RWMOL_SPTR matchedCore)
       : core_idx(core_index),
         numberMissingUserRGroups(numberMissingUserRGroups),
         rgroups(std::move(input_rgroups)),
diff --git a/Code/GraphMol/RGroupDecomposition/RGroupUtils.cpp b/Code/GraphMol/RGroupDecomposition/RGroupUtils.cpp
index a5e5548d468..63ffe83596c 100644
--- a/Code/GraphMol/RGroupDecomposition/RGroupUtils.cpp
+++ b/Code/GraphMol/RGroupDecomposition/RGroupUtils.cpp
@@ -110,13 +110,10 @@ bool isDummyRGroupAttachment(const Atom &atom) {
   if (isUserRLabel(atom)) {
     return true;
   }
-  int rlabel_type = 0;
-  bool unlabelled_core_attachment = false;
-  if (atom.hasProp(RLABEL) && atom.getPropIfPresent(RLABEL_TYPE, rlabel_type) &&
-      static_cast<Labelling>(rlabel_type) == Labelling::INDEX_LABELS &&
-      atom.getPropIfPresent(UNLABELLED_CORE_ATTACHMENT,
-                            unlabelled_core_attachment) &&
-      unlabelled_core_attachment) {
+  bool unlabeled_core_attachment = false;
+  if (atom.getPropIfPresent(UNLABELED_CORE_ATTACHMENT,
+                            unlabeled_core_attachment) &&
+      unlabeled_core_attachment) {
     return true;
   }
   return false;
diff --git a/Code/GraphMol/RGroupDecomposition/RGroupUtils.h b/Code/GraphMol/RGroupDecomposition/RGroupUtils.h
index 5bb764964d7..e683e4f1cad 100644
--- a/Code/GraphMol/RGroupDecomposition/RGroupUtils.h
+++ b/Code/GraphMol/RGroupDecomposition/RGroupUtils.h
@@ -22,9 +22,7 @@ RDKIT_RGROUPDECOMPOSITION_EXPORT extern const std::string RLABEL_CORE_INDEX;
 RDKIT_RGROUPDECOMPOSITION_EXPORT extern const std::string SIDECHAIN_RLABELS;
 RDKIT_RGROUPDECOMPOSITION_EXPORT extern const std::string done;
 RDKIT_RGROUPDECOMPOSITION_EXPORT extern const std::string
-    UNLABELLED_CORE_ATTACHMENT;
-RDKIT_RGROUPDECOMPOSITION_EXPORT extern const std::string
-    MISSING_RGROUP;
+    UNLABELED_CORE_ATTACHMENT;
 
 const unsigned int EMPTY_CORE_LABEL = -100000;
 
diff --git a/Code/GraphMol/RGroupDecomposition/Wrap/rdRGroupComposition.cpp b/Code/GraphMol/RGroupDecomposition/Wrap/rdRGroupComposition.cpp
index ce5084dced1..1b2e1d9bf0f 100644
--- a/Code/GraphMol/RGroupDecomposition/Wrap/rdRGroupComposition.cpp
+++ b/Code/GraphMol/RGroupDecomposition/Wrap/rdRGroupComposition.cpp
@@ -242,11 +242,11 @@ struct rgroupdecomp_wrapper {
         "              labelled.\n"
         "    - RGroupLabelling: choose where the rlabels are stored on the "
         "decomposition\n"
-        "                        RGroupLabels.AtomMap - store rgroups as atom "
+        "                        RGroupLabelling.AtomMap - store rgroups as atom "
         "maps (for smiles)\n"
-        "                        RGroupLabels.Isotope - store rgroups on the "
+        "                        RGroupLabelling.Isotope - store rgroups on the "
         "isotope\n"
-        "                        RGroupLabels.MDLRGroup - store rgroups as mdl "
+        "                        RGroupLabelling.MDLRGroup - store rgroups as mdl "
         "rgroups (for molblocks)\n"
         "                       default: AtomMap | MDLRGroup\n"
         "    - onlyMatchAtRGroups: only allow rgroup decomposition at the "
diff --git a/Code/GraphMol/RGroupDecomposition/catch_rgd.cpp b/Code/GraphMol/RGroupDecomposition/catch_rgd.cpp
index 9584c1bd956..ec783e26823 100644
--- a/Code/GraphMol/RGroupDecomposition/catch_rgd.cpp
+++ b/Code/GraphMol/RGroupDecomposition/catch_rgd.cpp
@@ -307,6 +307,7 @@ TEST_CASE("substructure parameters and RGD: chirality") {
     RGroupRows rows;
     std::vector<unsigned> unmatched;
     RGroupDecompositionParameters params;
+    params.allowMultipleRGroupsOnUnlabelled = true;
     {
       auto n = RGroupDecompose(cores, mols, rows, &unmatched, params);
       CHECK(n == mols.size());
@@ -315,17 +316,17 @@ TEST_CASE("substructure parameters and RGD: chirality") {
       CHECK(flatten_whitespace(toJSON(rows)) == flatten_whitespace(R"JSON(
 [
   {
-    "Core":"C1CC([*:1])([*:2])N1",
+    "Core":"C1C[C@@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"[H][*:2]"
   },
   {
-    "Core":"C1CC([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"O[*:2]"
   },
   {
-    "Core":"C1CC([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"[H][*:2]"
   },
@@ -370,6 +371,7 @@ TEST_CASE("substructure parameters and RGD: chirality") {
     RGroupRows rows;
     std::vector<unsigned> unmatched;
     RGroupDecompositionParameters params;
+    params.allowMultipleRGroupsOnUnlabelled = true;
     params.substructmatchParams.useChirality = false;
     {
       auto n = RGroupDecompose(cores, mols, rows, &unmatched, params);
@@ -447,6 +449,7 @@ TEST_CASE("substructure parameters and RGD: enhanced stereo") {
     RGroupRows rows;
     std::vector<unsigned> unmatched;
     RGroupDecompositionParameters params;
+    params.allowMultipleRGroupsOnUnlabelled = true;
     {
       auto n = RGroupDecompose(cores, mols, rows, &unmatched, params);
       CHECK(n == mols.size() - 1);
@@ -454,27 +457,27 @@ TEST_CASE("substructure parameters and RGD: enhanced stereo") {
       CHECK(unmatched.size() == mols.size() - n);
       // std::cerr << toJSON(rows) << std::endl;
 
-      // the core output here is SMARTS because the CXSMILES parser replaces the
-      // dummy atom with a query
+      // the core output no longer is SMARTS as the core output is the portion
+      // of the target that matches the core query.
       CHECK(flatten_whitespace(toJSON(rows)) == flatten_whitespace(R"JSON(
 [
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"[H][*:2]"
   },
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@@]([*:1])([*:2])N1",
     "R1":"O[*:1]",
     "R2":"F[*:2]"
   },
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@@]([*:1])([*:2])N1",
     "R1":"[H][*:1]",
     "R2":"F[*:2]"
   },
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@@]([*:1])([*:2])N1",
     "R1":"Cl[*:1]",
     "R2":"[H][*:2]"
   }
@@ -488,27 +491,27 @@ TEST_CASE("substructure parameters and RGD: enhanced stereo") {
       CHECK(unmatched.size() == 1);
       // std::cerr << toJSON(rows) << std::endl;
 
-      // the core output here is SMARTS because the CXSMILES parser replaces the
-      // dummy atom with a query
+      // the core output no longer is SMARTS as the core output is the portion
+      // of the target that matches the core query.
       CHECK(flatten_whitespace(toJSON(rows)) == flatten_whitespace(R"JSON(
 [
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"[H][*:2]"
   },
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@@]([*:1])([*:2])N1",
     "R1":"O[*:1]",
     "R2":"F[*:2]"
   },
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@@]([*:1])([*:2])N1",
     "R1":"[H][*:1]",
     "R2":"F[*:2]"
   },
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@@]([*:1])([*:2])N1",
     "R1":"Cl[*:1]",
     "R2":"[H][*:2]"
   }
@@ -521,6 +524,7 @@ TEST_CASE("substructure parameters and RGD: enhanced stereo") {
     RGroupRows rows;
     std::vector<unsigned> unmatched;
     RGroupDecompositionParameters params;
+    params.allowMultipleRGroupsOnUnlabelled = true;
     params.substructmatchParams.useEnhancedStereo = true;
     {
       auto n = RGroupDecompose(cores, mols, rows, &unmatched, params);
@@ -528,22 +532,22 @@ TEST_CASE("substructure parameters and RGD: enhanced stereo") {
       CHECK(rows.size() == n);
       CHECK(unmatched.size() == mols.size() - n);
       // std::cerr << toJSON(rows) << std::endl;
-      // the core output here is SMARTS because the CXSMILES parser replaces the
-      // dummy atom with a query
+      // the core output no longer is SMARTS as the core output is the portion
+      // of the target that matches the core query.
       CHECK(flatten_whitespace(toJSON(rows)) == flatten_whitespace(R"JSON(
 [
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"[H][*:2]"
   },
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"O[*:2]"
   },
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"[H][*:2]"
   }
@@ -556,27 +560,27 @@ TEST_CASE("substructure parameters and RGD: enhanced stereo") {
       CHECK(rows.size() == n);
       CHECK(unmatched.size() == 1);
       // std::cerr << toJSON(rows) << std::endl;
-      // the core output here is SMARTS because the CXSMILES parser replaces the
-      // dummy atom with a query
+      // the core output no longer is SMARTS as the core output is the portion
+      // of the target that matches the core query.
       CHECK(flatten_whitespace(toJSON(rows)) == flatten_whitespace(R"JSON(
 [
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"[H][*:2]"
   },
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"O[*:2]"
   },
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"[H][*:2]"
   },
   {
-    "Core":"[#6]1-[#6]-[#7]-[#6@]-1(-[!#1:1])-[#0:2]",
+    "Core":"C1C[C@@]([*:1])([*:2])N1",
     "R1":"Cl[*:1]",
     "R2":"[H][*:2]"
   }
@@ -615,6 +619,7 @@ TEST_CASE("rgroupLabelling") {
     std::vector<unsigned> unmatched;
     RGroupDecompositionParameters params;
     params.rgroupLabelling = RGroupLabelling::Isotope;
+    params.allowMultipleRGroupsOnUnlabelled = true;
     {
       auto n = RGroupDecompose(cores, mols, rows, &unmatched, params);
       CHECK(n == mols.size());
@@ -623,17 +628,17 @@ TEST_CASE("rgroupLabelling") {
       CHECK(flatten_whitespace(toJSON(rows)) == flatten_whitespace(R"JSON(
 [
   {
-    "Core":"[1*]C1([2*])CCN1",
+    "Core": "[1*][C@@]1([2*])CCN1",
     "R1":"[1*]F",
     "R2":"[2*][H]"
   },
   {
-    "Core":"[1*]C1([2*])CCN1",
+    "Core": "[1*][C@]1([2*])CCN1",
     "R1":"[1*]F",
     "R2":"[2*]O"
   },
   {
-    "Core":"[1*]C1([2*])CCN1",
+    "Core":"[1*][C@]1([2*])CCN1",
     "R1":"[1*]F",
     "R2":"[2*][H]"
   },
@@ -651,12 +656,16 @@ TEST_CASE("rgroupLabelling") {
     std::vector<unsigned> unmatched;
     RGroupDecompositionParameters params;
     params.rgroupLabelling = RGroupLabelling::MDLRGroup;
+    params.allowMultipleRGroupsOnUnlabelled = true;
     {
       auto n = RGroupDecompose(cores, mols, rows, &unmatched, params);
       CHECK(n == mols.size());
       CHECK(rows.size() == n);
       CHECK(unmatched.empty());
       // in this case the labels don't show up in the output SMILES
+      // Presumably the dummy atoms are no longer distinguishable without
+      // the isotope labels as the smiles no longer contains chiralty.
+      // Chirality is present in the core SMARTS
       CHECK(flatten_whitespace(toJSON(rows)) == flatten_whitespace(R"JSON(
 [
   {
@@ -687,6 +696,7 @@ TEST_CASE("rgroupLabelling") {
     RGroupRows rows;
     std::vector<unsigned> unmatched;
     RGroupDecompositionParameters params;
+    params.allowMultipleRGroupsOnUnlabelled = true;
     params.rgroupLabelling =
         RGroupLabelling::Isotope | RGroupLabelling::AtomMap;
     {
@@ -698,17 +708,17 @@ TEST_CASE("rgroupLabelling") {
 
 [
   {
-    "Core":"C1CC([1*:1])([2*:2])N1",
+    "Core":"C1C[C@@]([1*:1])([2*:2])N1",
     "R1":"F[1*:1]",
     "R2":"[H][2*:2]"
   },
   {
-    "Core":"C1CC([1*:1])([2*:2])N1",
+    "Core":"C1C[C@]([1*:1])([2*:2])N1",
     "R1":"F[1*:1]",
     "R2":"O[2*:2]"
   },
   {
-    "Core":"C1CC([1*:1])([2*:2])N1",
+    "Core":"C1C[C@]([1*:1])([2*:2])N1",
     "R1":"F[1*:1]",
     "R2":"[H][2*:2]"
   },