def _consolidate_intersections_rebuild_graph(  # noqa: C901,PLR0912,PLR0915
    G: nx.MultiDiGraph,
    tolerance: float | dict[int, float],
    reconnect_edges: bool,  # noqa: FBT001
    node_attr_aggs: dict[str, Any] | None,
) -> nx.MultiDiGraph:
    """
    Consolidate intersections comprising clusters of nearby nodes.

    Merge nodes and return a rebuilt graph with consolidated intersections and
    reconnected edge geometries.

    Parameters
    ----------
    G
        A projected graph.
    tolerance
        Nodes are buffered to this distance (in graph's geometry's units) and
        subsequent overlaps are dissolved into a single node. If scalar, then
        that single value will be used for all nodes. If dict (mapping node
        IDs to individual values), then those values will be used per node and
        any missing node IDs will not be buffered.
    reconnect_edges
        If True, reconnect edges (and their geometries) to the consolidated
        nodes in rebuilt graph, and update the edge length attributes. If
        False, the returned graph has no edges (which is faster if you just
        need topologically consolidated intersection counts).
    node_attr_aggs
        Allows user to aggregate node attributes values when merging nodes.
        Keys are node attribute names and values are aggregation functions
        (anything accepted as an argument by `pandas.agg`). Node attributes
        not in `node_attr_aggs` will contain the unique values across the
        merged nodes. If None, defaults to `{"elevation": "mean"}`.

    Returns
    -------
    Gc
        A rebuilt graph with consolidated intersections and (optionally)
        reconnected edge geometries.
    """
    if G.graph.get("consolidated"):  # pragma: no cover
        msg = "This graph has already been consolidated, cannot consolidate it again."
        raise GraphSimplificationError(msg)

    # default node attributes to aggregate upon consolidation
    if node_attr_aggs is None:
        node_attr_aggs = {"elevation": "mean"}

    # STEP 1
    # buffer nodes to passed-in distance and merge overlaps. turn merged nodes
    # into gdf and get centroids of each cluster as x, y
    node_clusters = gpd.GeoDataFrame(geometry=_merge_nodes_geometric(G, tolerance))
    centroids = node_clusters.centroid
    node_clusters["x"] = centroids.x
    node_clusters["y"] = centroids.y

    # STEP 2
    # attach each node to its cluster of merged nodes. first get the original
    # graph's node points then spatial join to give each node the label of
    # cluster it's within. make cluster labels type string.
    node_points = convert.graph_to_gdfs(G, edges=False).drop(columns=["x", "y"])
    gdf = gpd.sjoin(node_points, node_clusters, how="left", predicate="within")
    gdf = gdf.drop(columns="geometry").rename(columns={"index_right": "cluster"})
    gdf["cluster"] = gdf["cluster"].astype(str)

    # STEP 3
    # if a cluster contains multiple components (i.e., it's not connected)
    # move each component to its own cluster (otherwise you will connect
    # nodes together that are not truly connected, e.g., nearby deadends or
    # surface streets with bridge).
    for cluster_label, nodes_subset in gdf.groupby("cluster"):
        if len(nodes_subset) > 1:
            # identify all the (weakly connected) component in cluster
            wccs = list(nx.weakly_connected_components(G.subgraph(nodes_subset.index)))
            if len(wccs) > 1:
                # if there are multiple components in this cluster
                for suffix, wcc in enumerate(wccs):
                    # set subcluster xy to the centroid of just these nodes
                    idx = list(wcc)
                    subcluster_centroid = node_points.loc[idx].union_all().centroid
                    gdf.loc[idx, "x"] = subcluster_centroid.x
                    gdf.loc[idx, "y"] = subcluster_centroid.y
                    # move to subcluster by appending suffix to cluster label
                    gdf.loc[idx, "cluster"] = f"{cluster_label}-{suffix}"

    # give nodes unique integer IDs (subclusters with suffixes are strings)
    gdf["cluster"] = gdf["cluster"].factorize()[0]

    # STEP 4
    # create new empty graph and copy over misc graph data
    Gc = nx.MultiDiGraph()
    Gc.graph = G.graph

    # STEP 5
    # create a new node for each cluster of merged nodes
    # regroup now that we potentially have new cluster labels from step 3
    groups = gdf.groupby("cluster")
    for cluster_label, nodes_subset in groups:
        osmids = nodes_subset.index.to_list()
        if len(osmids) == 1:
            # if cluster is a single node, add that node to new graph
            osmid = osmids[0]
            Gc.add_node(cluster_label, osmid_original=osmid, **G.nodes[osmid])
        else:
            # if cluster is multiple merged nodes, create one new node with
            # attributes to represent the merged nodes' non-null values
            node_attrs = {
                "osmid_original": osmids,
                "x": nodes_subset["x"].iloc[0],
                "y": nodes_subset["y"].iloc[0],
            }
            for col in set(nodes_subset.columns):
                # get the unique non-null values (we won't add null attrs)
                unique_vals = list(set(nodes_subset[col].dropna()))
                if len(unique_vals) > 0 and col in node_attr_aggs:
                    # if this attribute's values must be aggregated, do so now
                    node_attrs[col] = nodes_subset[col].agg(node_attr_aggs[col])
                elif col == "street_count":
                    # if user doesn't specifically handle street_count with an
                    # agg function, just skip it here then calculate it later
                    continue
                elif len(unique_vals) == 1:
                    # if there's 1 unique value for this attribute, keep it
                    node_attrs[col] = unique_vals[0]
                elif len(unique_vals) > 1:
                    # if there are multiple unique values, keep one of each
                    node_attrs[col] = unique_vals
            Gc.add_node(cluster_label, **node_attrs)

    # mark the graph as having been consolidated
    G.graph["consolidated"] = True

    if len(G.edges) == 0 or not reconnect_edges:
        # if reconnect_edges is False or there are no edges in original graph
        # (after dead-end removed), then skip edges and return new graph as-is
        return Gc

    # STEP 6
    # create new edge from cluster to cluster for each edge in original graph
    gdf_edges = convert.graph_to_gdfs(G, nodes=False)
    for u, v, k, data in G.edges(keys=True, data=True):
        u2 = gdf.loc[u, "cluster"]
        v2 = gdf.loc[v, "cluster"]

        # only create the edge if we're not connecting the cluster
        # to itself, but always add original self-loops
        if (u2 != v2) or (u == v):
            data["u_original"] = u
            data["v_original"] = v
            if "geometry" not in data:
                data["geometry"] = gdf_edges.loc[(u, v, k), "geometry"]
            Gc.add_edge(u2, v2, **data)

    # STEP 7
    # for every group of merged nodes with more than 1 node in it, extend the
    # edge geometries to reach the new node point
    for cluster_label, nodes_subset in groups:
        # but only if there were multiple nodes merged together,
        # otherwise it's the same old edge as in original graph
        if len(nodes_subset) > 1:
            # get coords of merged nodes point centroid to prepend or
            # append to the old edge geom's coords
            x = Gc.nodes[cluster_label]["x"]
            y = Gc.nodes[cluster_label]["y"]
            xy = [(x, y)]

            # for each edge incident on this new merged node, update its
            # geometry to extend to/from the new node's point coords
            in_edges = set(Gc.in_edges(cluster_label, keys=True))
            out_edges = set(Gc.out_edges(cluster_label, keys=True))
            for u, v, k in in_edges | out_edges:
                old_coords = list(Gc.edges[u, v, k]["geometry"].coords)
                new_coords = xy + old_coords if cluster_label == u else old_coords + xy
                new_geom = LineString(new_coords)
                Gc.edges[u, v, k]["geometry"] = new_geom

                # update the edge length attribute, given the new geometry
                Gc.edges[u, v, k]["length"] = new_geom.length

    # calculate street_count attribute for all nodes lacking it
    null_nodes = [n for n, sc in Gc.nodes(data="street_count") if sc is None]
    street_counts = stats.count_streets_per_node(Gc, nodes=null_nodes)
    nx.set_node_attributes(Gc, street_counts, name="street_count")

    return Gc

    Consolidate intersections comprising clusters of nearby nodes.

    Merge nodes and return a rebuilt graph with consolidated intersections and
    reconnected edge geometries.

    Parameters
    ----------
    G
        A projected graph.
    tolerance
        Nodes are buffered to this distance (in graph's geometry's units) and
        subsequent overlaps are dissolved into a single node. If scalar, then
        that single value will be used for all nodes. If dict (mapping node
        IDs to individual values), then those values will be used per node and
        any missing node IDs will not be buffered.
    reconnect_edges
        If True, reconnect edges (and their geometries) to the consolidated
        nodes in rebuilt graph, and update the edge length attributes. If
        False, the returned graph has no edges (which is faster if you just
        need topologically consolidated intersection counts).
    node_attr_aggs
        Allows user to aggregate node attributes values when merging nodes.
        Keys are node attribute names and values are aggregation functions
        (anything accepted as an argument by `pandas.agg`). Node attributes
        not in `node_attr_aggs` will contain the unique values across the
        merged nodes. If None, defaults to `{"elevation": "mean"}`.

    Returns
    -------
    Gc
        A rebuilt graph with consolidated intersections and (optionally)
        reconnected edge geometries.
    
osmnx/simplification.py