protected NR doSplit(NR n) {
		List<DBTreeRecord<?, ? extends NS>> children = new ArrayList<>(getChildrenOf(n));
		assert children.size() == maxChildren + 1;

		// S1
		Comparator<NS> axis = doChooseSplitAxis(children);

		// S2
		int index = doChooseSplitIndex(children, axis);

		// S3
		// NOTE: Children are already sorted on the chosen axis
		List<DBTreeRecord<?, ? extends NS>> firstGroup = children.subList(0, index);
		List<DBTreeRecord<?, ? extends NS>> secondGroup = children.subList(index, children.size());

		// Keep the first group under the same node, but re-compute its bounds
		// Move the second group to a new node
		NR n1 = n;
		NR n2 = nodeStore.create();
		// NOTE: Careful here not to remove anything from node 0's entry
		n2.setParentKey(n.getParentKey());
		doAddToCachedChildren(n.getParentKey(), n2, cachedNodeChildren);
		n2.setType(n.getType());

		// Update existing node's metadata
		n1.setShape(unionStream(firstGroup.stream().map(DBTreeRecord::getBounds)));
		n1.setChildCount(index);
		n1.setDataCount(firstGroup.stream().mapToInt(DBTreeRecord::getDataCount).sum());

		// Set new node's metadata
		n2.setShape(unionStream(secondGroup.stream().map(DBTreeRecord::getBounds)));
		n2.setChildCount(maxChildren + 1 - index);
		n2.setDataCount(secondGroup.stream().mapToInt(DBTreeRecord::getDataCount).sum());

		// Move split-off children to new node
		if (n2.getType() == NodeType.LEAF) {
			for (DBTreeRecord<?, ?> move : secondGroup) {
				@SuppressWarnings("unchecked")
				DR dMove = (DR) move;
				doSetParentKey(dMove, n2.getKey(), cachedDataChildren);
			}
		}
		else {
			for (DBTreeRecord<?, ?> move : secondGroup) {
				@SuppressWarnings("unchecked")
				NR nMove = (NR) move;
				doSetParentKey(nMove, n2.getKey(), cachedNodeChildren);
			}
		}
		return n2;
	}
The Split algorithm as defined in Section 4.2 of the paper.

@param n the node to split
@return the new node (containing the second group)
Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/spatial/AbstractRStarConstraintsTree.java