def mergeGroupsOfRedistrictingGroups(groupsOfRedistrictingGroups):
    mergedRedistrictingGroups = []
    with tqdm(total=len(groupsOfRedistrictingGroups)) as pbar:
        for groupOfRedistrictingGroups in groupsOfRedistrictingGroups:
            if len(groupOfRedistrictingGroups) == 1:
                mergedRedistrictingGroups.append(groupOfRedistrictingGroups[0])
            else:
                allBorderBlocks = []
                allBlocks = []
                for redistrictingGroup in groupOfRedistrictingGroups:
                    allBorderBlocks.extend(redistrictingGroup.borderChildren)
                    allBlocks.extend(redistrictingGroup.children)

                # assign block neighbors to former border blocks
                tqdm.write('      *** Starting a merge with {0} border blocks and {1} total blocks ***'.format(
                    len(allBorderBlocks), len(allBlocks)))
                for formerBorderBlock in allBorderBlocks:
                    assignNeighborBlocksFromCandidateBlocks(block=formerBorderBlock,
                                                            candidateBlocks=allBlocks)

                contiguousRegions = findContiguousGroupsOfGraphObjects(allBlocks)

                mergedRedistrictingGroupsForPrevious = []
                for contiguousRegion in contiguousRegions:
                    contiguousRegionGroup = RedistrictingGroup(childrenBlocks=contiguousRegion)
                    # assign block neighbors to former border blocks
                    for borderBlock in contiguousRegionGroup.borderChildren:
                        assignNeighborBlocksFromCandidateBlocks(block=borderBlock,
                                                                candidateBlocks=contiguousRegionGroup.children)
                    contiguousRegionGroup.validateBlockNeighbors()
                    mergedRedistrictingGroupsForPrevious.append(contiguousRegionGroup)
                mergedRedistrictingGroups.extend(mergedRedistrictingGroupsForPrevious)
            pbar.update(1)

    return mergedRedistrictingGroups
def reorganizeAtomicBlockBetweenRedistrictingGroups(redistrictingGroups):
    for redistrictingGroup in redistrictingGroups:
        for borderBlock in redistrictingGroup.borderChildren:
            borderBlock.removeNonIntersectingNeighbors()

    atomicBlockGroupDict = {}
    for redistrictingGroup in redistrictingGroups:
        atomicBlockGroupDict[redistrictingGroup.graphId] = redistrictingGroup.children.copy()

    atomicBlockGroups = atomicBlockGroupDict.values()
    for atomicBlockGroup in atomicBlockGroups:
        contiguousRegions = findContiguousGroupsOfGraphObjects(atomicBlockGroup)
        while len(contiguousRegions) > 1:
            smallestContiguousRegion = min(contiguousRegions,
                                           key=lambda contiguousRegion: len(contiguousRegion))
            smallestContiguousRegionPolygon = polygonFromMultipleGeometries(smallestContiguousRegion)

            otherRegion = None
            otherSplitChildrenList = [x for x in atomicBlockGroups if x is not atomicBlockGroup]
            for otherSplitChildren in otherSplitChildrenList:
                otherSplitChildrenPolygon = polygonFromMultipleGeometries(otherSplitChildren)
                if intersectingPolygons(smallestContiguousRegionPolygon, otherSplitChildrenPolygon):
                    otherRegion = otherSplitChildren
                    break

            if otherRegion is None:
                allBlocksInOtherSplits = [block for blockList in otherSplitChildrenList for block in blockList]
                closestBlock = findClosestGeometry(smallestContiguousRegionPolygon, allBlocksInOtherSplits)
                closestSplit = next((blockList for blockList in otherSplitChildrenList if closestBlock in blockList),
                                    None)
                otherRegion = closestSplit

            for childBlock in smallestContiguousRegion:
                atomicBlockGroup.remove(childBlock)
                childBlock.removeNeighborConnections()

                otherRegion.append(childBlock)
                assignNeighborBlocksFromCandidateBlocks(block=childBlock, candidateBlocks=otherRegion)
            contiguousRegions = findContiguousGroupsOfGraphObjects(atomicBlockGroup)

    for key, value in atomicBlockGroupDict.items():
        groupWithId = next((redistrictingGroup for redistrictingGroup in redistrictingGroups
                            if redistrictingGroup.graphId == key), None)
        groupWithId.children = value

    for redistrictingGroup in redistrictingGroups:
        redistrictingGroup.attachOrphanBlocksToClosestNeighbor()

    return redistrictingGroups
def mergeCandidatesIntoPreviousGroups(candidates):
    mergedCandidates = []
    for candidate in candidates:

        # group redistricting groups together based on previous parent
        parentDict = {}
        for redistrictingGroup in candidate:
            # if it doesn't have a previous parent, that means it wasn't broken up, so we will just let is pass through
            if redistrictingGroup.previousParentId is None:
                parentDict[redistrictingGroup.graphId] = [redistrictingGroup]
            else:
                if redistrictingGroup.previousParentId in parentDict:
                    parentDict[redistrictingGroup.previousParentId].append(
                        redistrictingGroup)
                else:
                    parentDict[redistrictingGroup.previousParentId] = [
                        redistrictingGroup
                    ]

        # merge the grouped groups together
        mergedRedistrictingGroups = []
        with tqdm(total=len(parentDict)) as pbar:
            for redistrictingGroupList in parentDict.values():
                if len(redistrictingGroupList) == 1:
                    mergedRedistrictingGroups.append(redistrictingGroupList[0])
                else:
                    allBorderBlocks = []
                    allBlocks = []
                    for redistrictingGroup in redistrictingGroupList:
                        allBorderBlocks.extend(
                            redistrictingGroup.borderChildren)
                        allBlocks.extend(redistrictingGroup.children)

                    # assign block neighbors to former border blocks
                    tqdm.write(
                        '      *** Starting a merge with {0} border blocks and {1} total blocks ***'
                        .format(len(allBorderBlocks), len(allBlocks)))
                    for formerBorderBlock in allBorderBlocks:
                        assignNeighborBlocksFromCandidateBlocks(
                            block=formerBorderBlock, candidateBlocks=allBlocks)

                    contiguousRegions = findContiguousGroupsOfGraphObjects(
                        allBlocks)

                    mergedRedistrictingGroupsForPrevious = []
                    for contiguousRegion in contiguousRegions:
                        contiguousRegionGroup = RedistrictingGroup(
                            childrenBlocks=contiguousRegion)
                        # assign block neighbors to former border blocks
                        for borderBlock in contiguousRegionGroup.borderChildren:
                            assignNeighborBlocksFromCandidateBlocks(
                                block=borderBlock,
                                candidateBlocks=contiguousRegionGroup.children)
                        contiguousRegionGroup.validateBlockNeighbors()
                        mergedRedistrictingGroupsForPrevious.append(
                            contiguousRegionGroup)
                    mergedRedistrictingGroups.extend(
                        mergedRedistrictingGroupsForPrevious)
                pbar.update(1)

        mergedCandidates.append(mergedRedistrictingGroups)

    return mergedCandidates