# Buggy code
def cluster_vertically_aligned_boxes(
    boxes,
    page_bbox,
    avg_font_pts,
    width,
    char_width,
    boxes_segments,
    boxes_curves,
    boxes_figures,
    page_width,
    combine,
):
    # Too many "." in the Table of Content pages
    if len(boxes) == 0:
        log.warning("No boxes were found to cluster.")
        return [], []
    elif len(boxes) > 3500:
        log.warning("Too many '.' in the Table of Content pages?")
        return [], []

    plane = Plane(page_bbox)
    plane.extend(boxes)

    # initialize clusters
    cid2obj = [set([i]) for i in range(len(boxes))]
    # default object map to cluster with its own index
    obj2cid = list(range(len(boxes)))
    prev_clusters = obj2cid
    while True:
        for i1, b1 in enumerate(boxes):
            for i2, b2 in enumerate(boxes):
                if (i1 == i2) or (obj2cid[i1] == obj2cid[i2]):
                    continue
                if b1.bbox[1] < b2.bbox[1]:
                    box1 = b1.bbox
                    box2 = b2.bbox
                elif b2.bbox[1] < b1.bbox[1]:
                    box1 = b2.bbox
                    box2 = b1.bbox
                else:
                    # horizontally aligned
                    continue
                if (
                    box2[1] < box1[3]
                    or (box2[1] - box1[1] < 1.5 * avg_font_pts)
                    or (box2[3] - box1[3] < 1.5 * avg_font_pts)
                ):
                    # can probably do better if we find the average space
                    # between words
                    if (
                        abs(box1[0] - box2[0]) < 3
                        or abs(box1[2] - box2[2]) < 3
                        or (((box1[0] + box1[2]) / 2) == ((box2[0] + box2[2]) / 2))
                        or ((box1[0] < box2[0]) and (box1[2] > box2[0]))
                        or ((box1[0] > box2[0]) and (box2[2] > box1[0]))
                    ):
                        min_i = min(i1, i2)
                        max_i = max(i1, i2)
                        cid1 = obj2cid[min_i]
                        cid2 = obj2cid[max_i]
                        # move all objects from cluster cid2 to cid1
                        # reassign cluster ids for all such objects as well
                        for obj_iter in cid2obj[cid2]:
                            cid2obj[cid1].add(obj_iter)
                            obj2cid[obj_iter] = cid1
                        cid2obj[cid2] = set()
        if prev_clusters == obj2cid:
            break
        prev_clusters = obj2cid
    clusters = [[boxes[i] for i in cluster] for cluster in filter(bool, cid2obj)]

    rid2obj = [set([i]) for i in range(len(boxes))]  # initialize clusters
    # default object map to cluster with its own index
    obj2rid = list(range(len(boxes)))
    prev_clusters = obj2rid
    while True:
        for i1, b1 in enumerate(boxes):
            for i2, b2 in enumerate(boxes):
                if (i1 == i2) or (obj2rid[i1] == obj2rid[i2]):
                    continue
                box1 = b1.bbox
                box2 = b2.bbox
                if (
                    (abs(box1[1] - box2[1]) < 0.11 * avg_font_pts)
                    or (abs(box1[3] - box2[3]) < 0.11 * avg_font_pts)
                    or (
                        round((box1[1] + box1[3]) / 2) == round((box2[1] + box2[3]) / 2)
                    )
                ):
                    min_i = min(i1, i2)
                    max_i = max(i1, i2)
                    rid1 = obj2rid[min_i]
                    rid2 = obj2rid[max_i]
                    for obj_iter in rid2obj[rid2]:
                        rid2obj[rid1].add(obj_iter)
                        obj2rid[obj_iter] = rid1
                    rid2obj[rid2] = set()
        if prev_clusters == obj2rid:
            break
        prev_clusters = obj2rid

    not_merge = set()
    for i1, b1 in enumerate(boxes):
        for i2 in cid2obj[obj2cid[i1]]:
            if i1 == i2:
                continue
            row1 = obj2rid[i1]
            row2 = obj2rid[i2]
            if row1 == row2:
                continue
            if b1.bbox[1] < b2.bbox[1]:
                box1 = b1.bbox
                box2 = b2.bbox
            elif b2.bbox[1] < b1.bbox[1]:
                box1 = b2.bbox
                box2 = b1.bbox
            else:
                # horizontally aligned
                continue
            text_1 = 0.0
            for obj in rid2obj[row1]:
                text_1 += boxes[obj].bbox[2] - boxes[obj].bbox[0]
            text_2 = 0.0
            for obj in rid2obj[row2]:
                text_2 += boxes[obj].bbox[2] - boxes[obj].bbox[0]
            if abs(text_1 - text_2) / width > 0.1:
                min_i = min(i1, i2)
                max_i = max(i1, i2)
                not_merge.add((min_i, max_i))

    # Alignment Features
    # If text boxes are very close in a row
    if_row_connected = defaultdict(int)
    num_row_connected = defaultdict(lambda: 1)
    # If text is merged using span code in adjacent rows, this feature tells the number of times the cluster went through span based clustering
    if_connected_by_span = defaultdict(int)
    num_connected_by_span = defaultdict(lambda: 1)
    # If columns were merged using cluster alignment
    if_connected_by_align = defaultdict(int)
    num_connected_by_align = defaultdict(lambda: 1)
    # If vertical columns were merged
    if_vertical_columns_merged = defaultdict(int)
    num_vertical_columns_merged = defaultdict(lambda: 1)
    # Number of Line Segments, Curves and Figures
    num_segments = defaultdict(int)
    num_curves = defaultdict(int)
    num_figures = defaultdict(int)
    # Average Word Space
    total_word_space = defaultdict(float)
    avg_word_space = defaultdict(float)
    avg_word_space_norm = defaultdict(float)
    node_space = defaultdict(float)
    avg_node_space = defaultdict(float)
    avg_node_space_norm = defaultdict(float)

    cid2obj = [set([i]) for i in range(len(boxes))]  # initialize clusters
    obj2cid = list(
        range(len(boxes))
    )  # default object map to cluster with its own index
    prev_clusters = obj2cid
    # add the code for merging close text boxes in particular row
    while True:
        for i1, b1 in enumerate(boxes):
            for i2, b2 in enumerate(boxes):
                if (i1 == i2) or (obj2cid[i1] == obj2cid[i2]):
                    continue
                box1 = b1.bbox
                box2 = b2.bbox
                if obj2rid[i1] == obj2rid[i2]:
                    if (
                        (b1.bbox[0] < b2.bbox[0])
                        and ((b2.bbox[0] - b1.bbox[2]) <= 2 * char_width)
                    ) or (
                        (b2.bbox[0] < b1.bbox[0])
                        and ((b1.bbox[0] - b2.bbox[2]) <= 2 * char_width)
                    ):
                        min_i = min(i1, i2)
                        max_i = max(i1, i2)
                        cid1 = obj2cid[min_i]
                        cid2 = obj2cid[max_i]
                        for obj_iter in cid2obj[cid2]:
                            cid2obj[cid1].add(obj_iter)
                            obj2cid[obj_iter] = cid1
                        cid2obj[cid2] = set()
                        # Features
                        if_row_connected[cid1] = 1
                        if_row_connected[cid2] = 0
                        num_row_connected[cid1] += num_row_connected[cid2]
                        num_row_connected[cid2] = 0
        if prev_clusters == obj2cid:
            break
        prev_clusters = obj2cid

    # vertical alignment code
    while True:
        for i1, b1 in enumerate(boxes):
            for i2, b2 in enumerate(boxes):
                if (i1 == i2) or (obj2cid[i1] == obj2cid[i2]):
                    continue
                if b1.bbox[1] < b2.bbox[1]:
                    box1 = b1.bbox
                    box2 = b2.bbox
                elif b2.bbox[1] < b1.bbox[1]:
                    box1 = b2.bbox
                    box2 = b1.bbox
                else:
                    # horizontally aligned
                    continue
                if (
                    box2[1] < box1[3]
                    or (box2[1] - box1[1] < 1.5 * avg_font_pts)
                    or (box2[3] - box1[3] < 1.5 * avg_font_pts)
                ):  # can probably do better if we find the average space between words
                    if (
                        abs(box1[0] - box2[0]) < 3
                        or abs(box1[2] - box2[2]) < 3
                        or (((box1[0] + box1[2]) / 2) == ((box2[0] + box2[2]) / 2))
                    ):  # or ((box1[0]<box2[0]) and (box1[2]>box2[0])) or ((box1[0]>box2[0]) and (box2[2]>box1[0]))): #added center alignemnt
                        min_i = min(i1, i2)
                        max_i = max(i1, i2)
                        if (min_i, max_i) not in not_merge:
                            cid1 = obj2cid[min_i]
                            cid2 = obj2cid[max_i]
                            # move all objects from cluster cid2 to cid1
                            # reassign cluster ids for all such objects as well
                            for obj_iter in cid2obj[cid2]:
                                cid2obj[cid1].add(obj_iter)
                                obj2cid[obj_iter] = cid1
                            cid2obj[cid2] = set()
                            # Features
                            if_connected_by_span[cid1] = 1
                            if_connected_by_span[cid2] = 0
                            if (
                                if_row_connected[cid1] == 1
                                or if_row_connected[cid2] == 1
                            ):
                                if_row_connected[cid1] = 1
                                num_row_connected[cid1] += num_row_connected[cid2]
                                num_row_connected[cid2] = 0
                                if_row_connected[cid2] = 0
                            num_connected_by_span[cid1] = (
                                num_connected_by_span[cid1]
                                + num_connected_by_span[cid2]
                            )
                            num_connected_by_span[cid2] = 0
        if prev_clusters == obj2cid:
            break
        prev_clusters = obj2cid

    # blacklist nearly half-page wide clusters before horizontal merging
    cid2obj2 = cid2obj[:]
    obj2cid2 = obj2cid[:]
    blacklist = set()
    blacklist_obj = set()
    for cid_iter in range(len(cid2obj2)):
        cid = cid2obj2[cid_iter]
        xmin = float("Inf")
        xmax = float("-Inf")
        for obj in cid:
            xmin = min(xmin, boxes[obj].bbox[0])
            xmax = max(xmax, boxes[obj].bbox[2])
        if ((xmax - xmin) > width / 2.75 and (xmax - xmin) < width / 2) or (
            (xmax - xmin) > 0.9 * width
        ):
            blacklist.add(cid_iter)
            for obj in cid:
                blacklist_obj.add(obj)
                for obj_iter in rid2obj[obj2rid[obj]]:
                    if (
                        boxes[obj_iter].bbox[0] >= xmin
                        and boxes[obj_iter].bbox[2] <= xmax
                    ):
                        blacklist_obj.add(obj_iter)

    # create a cluster span
    cid2span = {}
    for cid in range(len(cid2obj)):
        cid2span[cid] = {}
        cid2span[cid]["min_x"] = float("Inf")
        cid2span[cid]["min_y"] = float("Inf")
        cid2span[cid]["max_x"] = float("-Inf")
        cid2span[cid]["max_y"] = float("-Inf")
        for obj in cid2obj[cid]:
            cid2span[cid]["min_x"] = min(cid2span[cid]["min_x"], boxes[obj].bbox[0])
            cid2span[cid]["max_x"] = max(cid2span[cid]["max_x"], boxes[obj].bbox[2])
            cid2span[cid]["min_y"] = min(cid2span[cid]["min_y"], boxes[obj].bbox[1])
            cid2span[cid]["max_y"] = max(cid2span[cid]["max_y"], boxes[obj].bbox[3])

    cid2cid = {}
    cid_pair_compared = set()
    cid2cid2 = [cid for cid in range(len(cid2obj))]
    for i1, b1 in enumerate(boxes):
        for i2, b2 in enumerate(boxes):
            if i1 == i2:
                continue
            if i1 in blacklist_obj or i2 in blacklist_obj:
                continue
            cid1 = obj2cid[i1]
            cid2 = obj2cid[i2]
            if (min(cid1, cid2), max(cid1, cid2)) in cid_pair_compared:
                continue
            if cid1 == cid2:
                continue
            if obj2rid[i1] == obj2rid[i2]:
                continue
            if cid1 not in cid2cid:
                cid2cid[cid1] = set()
            if cid2 not in cid2cid:
                cid2cid[cid2] = set()
            if cid2span[cid1]["min_y"] < cid2span[cid2]["min_y"]:
                box1 = [
                    cid2span[cid1]["min_x"],
                    cid2span[cid1]["min_y"],
                    cid2span[cid1]["max_x"],
                    cid2span[cid1]["max_y"],
                ]
                box2 = [
                    cid2span[cid2]["min_x"],
                    cid2span[cid2]["min_y"],
                    cid2span[cid2]["max_x"],
                    cid2span[cid2]["max_y"],
                ]
            else:
                box1 = [
                    cid2span[cid2]["min_x"],
                    cid2span[cid2]["min_y"],
                    cid2span[cid2]["max_x"],
                    cid2span[cid2]["max_y"],
                ]
                box2 = [
                    cid2span[cid1]["min_x"],
                    cid2span[cid1]["min_y"],
                    cid2span[cid1]["max_x"],
                    cid2span[cid1]["max_y"],
                ]
            if ((box1[1] < box2[1]) and (box1[3] > box2[1])) or (
                (box1[1] > box2[1]) and (box1[1] < box2[3])
            ):
                continue
            cid_pair_compared.add((min(cid1, cid2), max(cid1, cid2)))
            query_rect = (
                min(box1[0], box2[0]),
                min(box1[1], box2[1]),
                max(box1[2], box2[2]),
                max(box1[3], box2[3]),
            )
            connected = True
            for i3, b3 in enumerate(boxes):
                if (i3 == i1) or (i3 == i2):
                    continue
                if obj2cid[i1] == obj2cid[i3] or obj2cid[i2] == obj2cid[i3]:
                    continue
                box3 = b3.bbox
                if intersect(query_rect, box3):
                    connected = False
                    break
            if (
                (
                    (
                        round(box1[0]) == round(box2[0])
                        or round(box1[2]) == round(box2[2])
                    )
                    and connected
                )
                or (
                    round((box1[0] + box1[2]) / 2) == round((box2[0] + box2[2]) / 2)
                    and connected
                )
            ):  # or (abs((box1[0]+box1[2])/2-(box2[0]+box2[2])/2)<0.1*char_width and connected)):# or ((box1[0]<box2[0]) and (box1[2]>box2[0])) or ((box1[0]>box2[0]) and (box2[2]>box1[0]))): #added center alignemnt
                cid2cid[min(cid1, cid2)].add(max(cid1, cid2))
                min_cid = min(cid1, cid2)
                max_cid = max(cid1, cid2)
                for cid_iter in cid2cid2:
                    if cid2cid2[cid_iter] == cid2cid2[max_cid]:
                        cid2cid2[cid_iter] = cid2cid2[min_cid]

    # post-process cid2cid
    cid2obj2 = cid2obj[:]
    obj2cid2 = obj2cid[:]
    for cid in range(len(cid2cid2)):
        cid_merge = cid2cid2[cid]
        if cid != cid_merge:
            for obj_iter in cid2obj2[cid]:
                cid2obj2[cid_merge].add(obj_iter)
                obj2cid2[obj_iter] = cid_merge
            cid2obj2[cid] = set()
            # Features
            if_connected_by_align[cid_merge] = 1
            if_connected_by_align[cid] = 0
            if if_row_connected[cid_merge] == 1 or if_row_connected[cid] == 1:
                if_row_connected[cid_merge] = 1
                num_row_connected[cid_merge] += num_row_connected[cid]
                num_row_connected[cid] = 0
                if_row_connected[cid2] = 0
            if if_connected_by_span[cid_merge] == 1 or if_connected_by_span[cid] == 1:
                if_connected_by_span[cid_merge] = 1
                num_connected_by_span[cid_merge] += num_connected_by_span[cid]
                num_connected_by_span[cid] = 0
                if_connected_by_span[cid] = 0
            num_connected_by_align[cid_merge] += num_connected_by_align[cid]
            num_connected_by_align[cid] = 0

            # code to merge columns for table
    prev_clusters = obj2cid2
    while True:
        for obj1, b1 in enumerate(boxes):
            cid1 = obj2cid2[obj1]
            rid1 = obj2rid[obj1]
            if cid1 in blacklist:
                continue
            if obj1 in blacklist_obj:
                continue
            for obj2, b2 in enumerate(boxes):
                if obj1 == obj2:
                    continue
                if obj2cid2[obj2] == cid1:
                    rid2 = obj2rid[obj2]
                    if rid1 == rid2:
                        continue
                    for obj3 in rid2obj[rid2]:
                        cid3 = obj2cid2[obj3]
                        if obj3 in blacklist_obj:
                            continue
                        if cid1 != cid3:
                            for obj4 in cid2obj2[cid3]:
                                if obj4 == obj3:
                                    continue
                                if obj2rid[obj4] == rid1:
                                    min_cid = min(cid1, cid3)
                                    max_cid = max(cid1, cid3)
                                    for obj_iter in cid2obj2[max_cid]:
                                        cid2obj2[min_cid].add(obj_iter)
                                        obj2cid2[obj_iter] = min_cid
                                    cid2obj2[max_cid] = set()
                                    # Features
                                    if_vertical_columns_merged[min_cid] = 1
                                    if_vertical_columns_merged[max_cid] = 0
                                    num_vertical_columns_merged[min_cid] += (
                                        num_vertical_columns_merged[max_cid]
                                    )
                                    num_vertical_columns_merged[max_cid] = 0
                                    if (
                                        if_row_connected[min_cid] == 1
                                        or if_row_connected[max_cid] == 1
                                    ):
                                        if_row_connected[min_cid] = 1
                                        num_row_connected[min_cid] += num_row_connected[
                                            max_cid
                                        ]
                                        num_row_connected[max_cid] = 0
                                        if_row_connected[max_cid] = 0
                                    if (
                                        if_connected_by_span[min_cid] == 1
                                        or if_connected_by_span[max_cid] == 1
                                    ):
                                        if_connected_by_span[min_cid] = 1
                                        num_connected_by_span[min_cid] += (
                                            num_connected_by_span[max_cid]
                                        )
                                        num_connected_by_span[max_cid] = 0
                                        if_connected_by_span[max_cid] = 0
                                    if (
                                        if_connected_by_align[min_cid] == 1
                                        or if_connected_by_align[max_cid] == 1
                                    ):
                                        if_connected_by_align[min_cid] = 1
                                        num_connected_by_align[min_cid] += (
                                            num_connected_by_align[max_cid]
                                        )
                                        num_connected_by_align[max_cid] = 0
                                        if_connected_by_align[max_cid] = 0
                                    break
        if prev_clusters == obj2cid2:
            break
        prev_clusters = obj2cid2

    clusters = [[boxes[i] for i in cluster] for cluster in filter(bool, cid2obj2)]
    nodes = [Node(elems) for elems in clusters]
    node_indices = [i for i, x in enumerate(cid2obj2) if x]
    merge_indices = [i for i in range(len(node_indices))]
    page_stat = Node(boxes)
    nodes, merge_indices = merge_nodes(nodes, plane, page_stat, merge_indices)
    # Features
    for idx in range(len(merge_indices)):
        if merge_indices[idx] != idx:
            cid1 = node_indices[merge_indices[idx]]
            cid2 = node_indices[idx]
            if if_row_connected[cid1] == 1 or if_row_connected[cid2] == 1:
                if_row_connected[cid1] = 1
                num_row_connected[cid1] += num_row_connected[cid2]
                num_row_connected[cid2] = 0
                if_row_connected[cid2] = 0
            if if_connected_by_span[cid1] == 1 or if_connected_by_span[cid2] == 1:
                if_connected_by_span[cid1] = 1
                num_connected_by_span[cid1] += num_connected_by_span[cid2]
                num_connected_by_span[cid2] = 0
                if_connected_by_span[cid2] = 0
            if if_connected_by_align[cid1] == 1 or if_connected_by_align[cid2] == 1:
                if_connected_by_align[cid1] = 1
                num_connected_by_align[cid1] += num_connected_by_align[cid2]
                num_connected_by_align[cid2] = 0
                if_connected_by_align[cid2] = 0
            if (
                if_vertical_columns_merged[cid1] == 1
                or if_vertical_columns_merged[cid2] == 1
            ):
                if_vertical_columns_merged[cid1] = 1
                num_vertical_columns_merged[cid1] += num_vertical_columns_merged[cid2]
                num_vertical_columns_merged[cid2] = 0
                if_vertical_columns_merged[cid2] = 0

    # Get Word Spacing Features
    rid2space = defaultdict(float)
    rid2space_norm = defaultdict(float)
    row_indices = [i for i, x in enumerate(rid2obj) if x]
    for rid in row_indices:
        obj_list = list(rid2obj[rid])
        if len(obj_list) == 1:
            rid2space[rid] = 0
            continue
        obj_boxes = [boxes[obj].bbox[0] for obj in obj_list]
        sorted_obj_idx = [
            i[0] for i in sorted(enumerate(obj_boxes), key=lambda x: x[1])
        ]
        for obj_idx in range(len(sorted_obj_idx) - 1):
            rid2space[rid] += (
                boxes[obj_list[sorted_obj_idx[obj_idx + 1]]].bbox[2]
                - boxes[obj_list[sorted_obj_idx[obj_idx]]].bbox[0]
            )
        rid2space_norm[rid] = rid2space[rid] / (len(obj_list) - 1)

    for idx, node in enumerate(nodes):
        node_idx = node_indices[idx]
        if merge_indices[idx] == idx:
            obj_list = []
            for idx_iter in range(len(merge_indices)):
                if merge_indices[idx_iter] == idx:
                    obj_list += list(cid2obj2[node_indices[idx_iter]])
            obj_list = list(set(obj_list))
            rid_list = list(set([obj2rid[obj] for obj in obj_list]))
            for rid in rid_list:
                total_word_space[node_idx] += rid2space[rid]
                avg_word_space_norm[node_idx] += rid2space_norm[rid]
                obj_boxes = [
                    boxes[obj].bbox[0] for obj in rid2obj if obj in cid2obj2[node_idx]
                ]
                sorted_obj_idx = [
                    i[0] for i in sorted(enumerate(obj_boxes), key=lambda x: x[1])
                ]
                for obj_idx in range(len(sorted_obj_idx) - 1):
                    node_space[node_idx] += (
                        boxes[obj_list[sorted_obj_idx[obj_idx + 1]]].bbox[2]
                        - boxes[obj_list[sorted_obj_idx[obj_idx]]].bbox[0]
                    )
                avg_node_space_norm[node_idx] += node_space[node_idx] / (
                    len(obj_boxes) - 1
                )
            avg_word_space[node_idx] = total_word_space[node_idx] / len(rid_list)
            avg_word_space_norm[node_idx] /= len(rid_list)
            avg_node_space[node_idx] = node_space[node_idx] / len(rid_list)
            avg_node_space_norm[node_idx] /= len(rid_list)

    new_nodes = []
    new_node_indices = []
    for idx in range(len(merge_indices)):
        if merge_indices[idx] == idx:
            new_nodes.append(nodes[idx])
            new_node_indices.append(node_indices[idx])

    nodes = new_nodes
    node_indices = new_node_indices
    # Features
    for idx, node in enumerate(nodes):
        node_idx = node_indices[idx]
        node_bbox = (node.x0, node.y0, node.x1, node.y1)
        for i1, b1 in enumerate(boxes_segments):
            if intersect(node_bbox, b1.bbox):
                num_segments[node_idx] += 1
        for i1, b1 in enumerate(boxes_figures):
            if intersect(node_bbox, b1.bbox):
                num_figures[node_idx] += 1
        for i1, b1 in enumerate(boxes_curves):
            if intersect(node_bbox, b1.bbox):
                num_curves[node_idx] += 1

    tables = []
    table_indices = []
    for idx, node in enumerate(nodes):
        node_idx = node_indices[idx]
        isTable = True
        if node.is_table():
            for elem in node.elems:
                if "table" in elem.get_text().lower():
                    continue
                if (node.width - elem.bbox[2] + elem.bbox[0]) < 2 * char_width:
                    isTable = False
            if isTable:
                tables.append(node)
                table_indices.append(node_idx)

    if combine == True:
        node_features = [0] * 17
        for idx, node in enumerate(nodes):
            node_idx = node_indices[idx]
            node_features = [
                sum(x)
                for x in zip(
                    node_features,
                    [
                        if_row_connected[node_idx],
                        num_row_connected[node_idx],
                        if_connected_by_span[node_idx],
                        num_connected_by_span[node_idx],
                        if_connected_by_align[node_idx],
                        num_connected_by_align[node_idx],
                        if_vertical_columns_merged[node_idx],
                        num_vertical_columns_merged[node_idx],
                        num_segments[node_idx],
                        num_curves[node_idx],
                        num_figures[node_idx],
                        total_word_space[node_idx],
                        avg_word_space[node_idx],
                        avg_word_space_norm[node_idx],
                        node_space[node_idx],
                        avg_node_space[node_idx],
                        avg_node_space_norm[node_idx],
                    ],
                )
            ]
        return [], node_features
    else:
        table_features = []
        for idx, table in enumerate(tables):
            table_idx = table_indices[idx]
            table_features += [
                [
                    if_row_connected[table_idx],
                    num_row_connected[table_idx],
                    if_connected_by_span[table_idx],
                    num_connected_by_span[table_idx],
                    if_connected_by_align[table_idx],
                    num_connected_by_align[table_idx],
                    if_vertical_columns_merged[table_idx],
                    num_vertical_columns_merged[table_idx],
                    num_segments[table_idx],
                    num_curves[table_idx],
                    num_figures[table_idx],
                    total_word_space[table_idx],
                    avg_word_space[table_idx],
                    avg_word_space_norm[table_idx],
                    node_space[table_idx],
                    avg_node_space[table_idx],
                    avg_node_space_norm[table_idx],
                ]
            ]
        return tables, table_features


# Traceback (ValueError)
Digitized PDF detected, building tree structure
float division by zero
float division by zero
float division by zero
(89.33853599999992, 89.33853599999992, 50)
Traceback (most recent call last):
File "chh_test.py", line 10, in <module>
pdftotree.parse(filename, htmlpath, model_path=None, favor_figures=False, visualize=True)
File "/home/chhenning/repos/pdftotree/pdftotree/core.py", line 57, in parse
pdf_tree = extractor.get_tree_structure(model, favor_figures)
File "/home/chhenning/repos/pdftotree/pdftotree/TreeExtract.py", line 195, in get_tree_structure
favor_figures)
File "/home/chhenning/repos/pdftotree/pdftotree/pdf/pdf_parsers.py", line 615, in parse_tree_structure
figures_page = get_figures(mentions, elems.layout.bbox, page_num, boxes_figures, page_width, page_height)
File "/home/chhenning/repos/pdftotree/pdftotree/pdf/pdf_parsers.py", line 945, in get_figures
page_stat = Node(boxes)
File "/home/chhenning/repos/pdftotree/pdftotree/pdf/node.py", line 44, in __init__
self.set_bbox(bound_elems(elems))
File "/home/chhenning/repos/pdftotree/pdftotree/pdf/vector_utils.py", line 101, in bound_elems
group_x0 = min(map(lambda l:l.x0,elems))
ValueError: min() arg is an empty sequence

# Fixed code
def cluster_vertically_aligned_boxes(
    boxes,
    page_bbox,
    avg_font_pts,
    width,
    char_width,
    boxes_segments,
    boxes_curves,
    boxes_figures,
    page_width,
    combine,
):
    # Filter out boxes with zero width or height
    filtered_boxes = []
    for bbox in boxes:
        if bbox.x1 - bbox.x0 > 0 and bbox.y1 - bbox.y0 > 0:
            filtered_boxes.append(bbox)
    boxes = filtered_boxes

    # Too many "." in the Table of Content pages
    if len(boxes) == 0:
        log.warning("No boxes were found to cluster.")
        return [], []
    elif len(boxes) > 3500:
        log.warning("Too many '.' in the Table of Content pages?")
        return [], []

    plane = Plane(page_bbox)
    plane.extend(boxes)

    # initialize clusters
    cid2obj = [set([i]) for i in range(len(boxes))]
    # default object map to cluster with its own index
    obj2cid = list(range(len(boxes)))
    prev_clusters = obj2cid
    while True:
        for i1, b1 in enumerate(boxes):
            for i2, b2 in enumerate(boxes):
                if (i1 == i2) or (obj2cid[i1] == obj2cid[i2]):
                    continue
                if b1.bbox[1] < b2.bbox[1]:
                    box1 = b1.bbox
                    box2 = b2.bbox
                elif b2.bbox[1] < b1.bbox[1]:
                    box1 = b2.bbox
                    box2 = b1.bbox
                else:
                    # horizontally aligned
                    continue
                if (
                    box2[1] < box1[3]
                    or (box2[1] - box1[1] < 1.5 * avg_font_pts)
                    or (box2[3] - box1[3] < 1.5 * avg_font_pts)
                ):
                    # can probably do better if we find the average space
                    # between words
                    if (
                        abs(box1[0] - box2[0]) < 3
                        or abs(box1[2] - box2[2]) < 3
                        or (((box1[0] + box1[2]) / 2) == ((box2[0] + box2[2]) / 2))
                        or ((box1[0] < box2[0]) and (box1[2] > box2[0]))
                        or ((box1[0] > box2[0]) and (box2[2] > box1[0]))
                    ):
                        min_i = min(i1, i2)
                        max_i = max(i1, i2)
                        cid1 = obj2cid[min_i]
                        cid2 = obj2cid[max_i]
                        # move all objects from cluster cid2 to cid1
                        # reassign cluster ids for all such objects as well
                        for obj_iter in cid2obj[cid2]:
                            cid2obj[cid1].add(obj_iter)
                            obj2cid[obj_iter] = cid1
                        cid2obj[cid2] = set()
        if prev_clusters == obj2cid:
            break
        prev_clusters = obj2cid
    clusters = [[boxes[i] for i in cluster] for cluster in filter(bool, cid2obj)]

    rid2obj = [set([i]) for i in range(len(boxes))]  # initialize clusters
    # default object map to cluster with its own index
    obj2rid = list(range(len(boxes)))
    prev_clusters = obj2rid
    while True:
        for i1, b1 in enumerate(boxes):
            for i2, b2 in enumerate(boxes):
                if (i1 == i2) or (obj2rid[i1] == obj2rid[i2]):
                    continue
                box1 = b1.bbox
                box2 = b2.bbox
                if (
                    (abs(box1[1] - box2[1]) < 0.11 * avg_font_pts)
                    or (abs(box1[3] - box2[3]) < 0.11 * avg_font_pts)
                    or (
                        round((box1[1] + box1[3]) / 2) == round((box2[1] + box2[3]) / 2)
                    )
                ):
                    min_i = min(i1, i2)
                    max_i = max(i1, i2)
                    rid1 = obj2rid[min_i]
                    rid2 = obj2rid[max_i]
                    for obj_iter in rid2obj[rid2]:
                        rid2obj[rid1].add(obj_iter)
                        obj2rid[obj_iter] = rid1
                    rid2obj[rid2] = set()
        if prev_clusters == obj2rid:
            break
        prev_clusters = obj2rid

    not_merge = set()
    for i1, b1 in enumerate(boxes):
        for i2 in cid2obj[obj2cid[i1]]:
            if i1 == i2:
                continue
            row1 = obj2rid[i1]
            row2 = obj2rid[i2]
            if row1 == row2:
                continue
            if b1.bbox[1] < b2.bbox[1]:
                box1 = b1.bbox
                box2 = b2.bbox
            elif b2.bbox[1] < b1.bbox[1]:
                box1 = b2.bbox
                box2 = b1.bbox
            else:
                # horizontally aligned
                continue
            text_1 = 0.0
            for obj in rid2obj[row1]:
                text_1 += boxes[obj].bbox[2] - boxes[obj].bbox[0]
            text_2 = 0.0
            for obj in rid2obj[row2]:
                text_2 += boxes[obj].bbox[2] - boxes[obj].bbox[0]
            if abs(text_1 - text_2) / width > 0.1:
                min_i = min(i1, i2)
                max_i = max(i1, i2)
                not_merge.add((min_i, max_i))

    # Alignment Features
    # If text boxes are very close in a row
    if_row_connected = defaultdict(int)
    num_row_connected = defaultdict(lambda: 1)
    # If text is merged using span code in adjacent rows, this feature tells the number of times the cluster went through span based clustering
    if_connected_by_span = defaultdict(int)
    num_connected_by_span = defaultdict(lambda: 1)
    # If columns were merged using cluster alignment
    if_connected_by_align = defaultdict(int)
    num_connected_by_align = defaultdict(lambda: 1)
    # If vertical columns were merged
    if_vertical_columns_merged = defaultdict(int)
    num_vertical_columns_merged = defaultdict(lambda: 1)
    # Number of Line Segments, Curves and Figures
    num_segments = defaultdict(int)
    num_curves = defaultdict(int)
    num_figures = defaultdict(int)
    # Average Word Space
    total_word_space = defaultdict(float)
    avg_word_space = defaultdict(float)
    avg_word_space_norm = defaultdict(float)
    node_space = defaultdict(float)
    avg_node_space = defaultdict(float)
    avg_node_space_norm = defaultdict(float)

    cid2obj = [set([i]) for i in range(len(boxes))]  # initialize clusters
    obj2cid = list(
        range(len(boxes))
    )  # default object map to cluster with its own index
    prev_clusters = obj2cid
    # add the code for merging close text boxes in particular row
    while True:
        for i1, b1 in enumerate(boxes):
            for i2, b2 in enumerate(boxes):
                if (i1 == i2) or (obj2cid[i1] == obj2cid[i2]):
                    continue
                box1 = b1.bbox
                box2 = b2.bbox
                if obj2rid[i1] == obj2rid[i2]:
                    if (
                        (b1.bbox[0] < b2.bbox[0])
                        and ((b2.bbox[0] - b1.bbox[2]) <= 2 * char_width)
                    ) or (
                        (b2.bbox[0] < b1.bbox[0])
                        and ((b1.bbox[0] - b2.bbox[2]) <= 2 * char_width)
                    ):
                        min_i = min(i1, i2)
                        max_i = max(i1, i2)
                        cid1 = obj2cid[min_i]
                        cid2 = obj2cid[max_i]
                        for obj_iter in cid2obj[cid2]:
                            cid2obj[cid1].add(obj_iter)
                            obj2cid[obj_iter] = cid1
                        cid2obj[cid2] = set()
                        # Features
                        if_row_connected[cid1] = 1
                        if_row_connected[cid2] = 0
                        num_row_connected[cid1] += num_row_connected[cid2]
                        num_row_connected[cid2] = 0
        if prev_clusters == obj2cid:
            break
        prev_clusters = obj2cid

    # vertical alignment code
    while True:
        for i1, b1 in enumerate(boxes):
            for i2, b2 in enumerate(boxes):
                if (i1 == i2) or (obj2cid[i1] == obj2cid[i2]):
                    continue
                if b1.bbox[1] < b2.bbox[1]:
                    box1 = b1.bbox
                    box2 = b2.bbox
                elif b2.bbox[1] < b1.bbox[1]:
                    box1 = b2.bbox
                    box2 = b1.bbox
                else:
                    # horizontally aligned
                    continue
                if (
                    box2[1] < box1[3]
                    or (box2[1] - box1[1] < 1.5 * avg_font_pts)
                    or (box2[3] - box1[3] < 1.5 * avg_font_pts)
                ):  # can probably do better if we find the average space between words
                    if (
                        abs(box1[0] - box2[0]) < 3
                        or abs(box1[2] - box2[2]) < 3
                        or (((box1[0] + box1[2]) / 2) == ((box2[0] + box2[2]) / 2))
                    ):  # or ((box1[0]<box2[0]) and (box1[2]>box2[0])) or ((box1[0]>box2[0]) and (box2[2]>box1[0]))): #added center alignemnt
                        min_i = min(i1, i2)
                        max_i = max(i1, i2)
                        if (min_i, max_i) not in not_merge:
                            cid1 = obj2cid[min_i]
                            cid2 = obj2cid[max_i]
                            # move all objects from cluster cid2 to cid1
                            # reassign cluster ids for all such objects as well
                            for obj_iter in cid2obj[cid2]:
                                cid2obj[cid1].add(obj_iter)
                                obj2cid[obj_iter] = cid1
                            cid2obj[cid2] = set()
                            # Features
                            if_connected_by_span[cid1] = 1
                            if_connected_by_span[cid2] = 0
                            if (
                                if_row_connected[cid1] == 1
                                or if_row_connected[cid2] == 1
                            ):
                                if_row_connected[cid1] = 1
                                num_row_connected[cid1] += num_row_connected[cid2]
                                num_row_connected[cid2] = 0
                                if_row_connected[cid2] = 0
                            num_connected_by_span[cid1] = (
                                num_connected_by_span[cid1]
                                + num_connected_by_span[cid2]
                            )
                            num_connected_by_span[cid2] = 0
        if prev_clusters == obj2cid:
            break
        prev_clusters = obj2cid

    # blacklist nearly half-page wide clusters before horizontal merging
    cid2obj2 = cid2obj[:]
    obj2cid2 = obj2cid[:]
    blacklist = set()
    blacklist_obj = set()
    for cid_iter in range(len(cid2obj2)):
        cid = cid2obj2[cid_iter]
        xmin = float("Inf")
        xmax = float("-Inf")
        for obj in cid:
            xmin = min(xmin, boxes[obj].bbox[0])
            xmax = max(xmax, boxes[obj].bbox[2])
        if ((xmax - xmin) > width / 2.75 and (xmax - xmin) < width / 2) or (
            (xmax - xmin) > 0.9 * width
        ):
            blacklist.add(cid_iter)
            for obj in cid:
                blacklist_obj.add(obj)
                for obj_iter in rid2obj[obj2rid[obj]]:
                    if (
                        boxes[obj_iter].bbox[0] >= xmin
                        and boxes[obj_iter].bbox[2] <= xmax
                    ):
                        blacklist_obj.add(obj_iter)

    # create a cluster span
    cid2span = {}
    for cid in range(len(cid2obj)):
        cid2span[cid] = {}
        cid2span[cid]["min_x"] = float("Inf")
        cid2span[cid]["min_y"] = float("Inf")
        cid2span[cid]["max_x"] = float("-Inf")
        cid2span[cid]["max_y"] = float("-Inf")
        for obj in cid2obj[cid]:
            cid2span[cid]["min_x"] = min(cid2span[cid]["min_x"], boxes[obj].bbox[0])
            cid2span[cid]["max_x"] = max(cid2span[cid]["max_x"], boxes[obj].bbox[2])
            cid2span[cid]["min_y"] = min(cid2span[cid]["min_y"], boxes[obj].bbox[1])
            cid2span[cid]["max_y"] = max(cid2span[cid]["max_y"], boxes[obj].bbox[3])

    cid2cid = {}
    cid_pair_compared = set()
    cid2cid2 = [cid for cid in range(len(cid2obj))]
    for i1, b1 in enumerate(boxes):
        for i2, b2 in enumerate(boxes):
            if i1 == i2:
                continue
            if i1 in blacklist_obj or i2 in blacklist_obj:
                continue
            cid1 = obj2cid[i1]
            cid2 = obj2cid[i2]
            if (min(cid1, cid2), max(cid1, cid2)) in cid_pair_compared:
                continue
            if cid1 == cid2:
                continue
            if obj2rid[i1] == obj2rid[i2]:
                continue
            if cid1 not in cid2cid:
                cid2cid[cid1] = set()
            if cid2 not in cid2cid:
                cid2cid[cid2] = set()
            if cid2span[cid1]["min_y"] < cid2span[cid2]["min_y"]:
                box1 = [
                    cid2span[cid1]["min_x"],
                    cid2span[cid1]["min_y"],
                    cid2span[cid1]["max_x"],
                    cid2span[cid1]["max_y"],
                ]
                box2 = [
                    cid2span[cid2]["min_x"],
                    cid2span[cid2]["min_y"],
                    cid2span[cid2]["max_x"],
                    cid2span[cid2]["max_y"],
                ]
            else:
                box1 = [
                    cid2span[cid2]["min_x"],
                    cid2span[cid2]["min_y"],
                    cid2span[cid2]["max_x"],
                    cid2span[cid2]["max_y"],
                ]
                box2 = [
                    cid2span[cid1]["min_x"],
                    cid2span[cid1]["min_y"],
                    cid2span[cid1]["max_x"],
                    cid2span[cid1]["max_y"],
                ]
            if ((box1[1] < box2[1]) and (box1[3] > box2[1])) or (
                (box1[1] > box2[1]) and (box1[1] < box2[3])
            ):
                continue
            cid_pair_compared.add((min(cid1, cid2), max(cid1, cid2)))
            query_rect = (
                min(box1[0], box2[0]),
                min(box1[1], box2[1]),
                max(box1[2], box2[2]),
                max(box1[3], box2[3]),
            )
            connected = True
            for i3, b3 in enumerate(boxes):
                if (i3 == i1) or (i3 == i2):
                    continue
                if obj2cid[i1] == obj2cid[i3] or obj2cid[i2] == obj2cid[i3]:
                    continue
                box3 = b3.bbox
                if intersect(query_rect, box3):
                    connected = False
                    break
            if (
                (
                    (
                        round(box1[0]) == round(box2[0])
                        or round(box1[2]) == round(box2[2])
                    )
                    and connected
                )
                or (
                    round((box1[0] + box1[2]) / 2) == round((box2[0] + box2[2]) / 2)
                    and connected
                )
            ):  # or (abs((box1[0]+box1[2])/2-(box2[0]+box2[2])/2)<0.1*char_width and connected)):# or ((box1[0]<box2[0]) and (box1[2]>box2[0])) or ((box1[0]>box2[0]) and (box2[2]>box1[0]))): #added center alignemnt
                cid2cid[min(cid1, cid2)].add(max(cid1, cid2))
                min_cid = min(cid1, cid2)
                max_cid = max(cid1, cid2)
                for cid_iter in cid2cid2:
                    if cid2cid2[cid_iter] == cid2cid2[max_cid]:
                        cid2cid2[cid_iter] = cid2cid2[min_cid]

    # post-process cid2cid
    cid2obj2 = cid2obj[:]
    obj2cid2 = obj2cid[:]
    for cid in range(len(cid2cid2)):
        cid_merge = cid2cid2[cid]
        if cid != cid_merge:
            for obj_iter in cid2obj2[cid]:
                cid2obj2[cid_merge].add(obj_iter)
                obj2cid2[obj_iter] = cid_merge
            cid2obj2[cid] = set()
            # Features
            if_connected_by_align[cid_merge] = 1
            if_connected_by_align[cid] = 0
            if if_row_connected[cid_merge] == 1 or if_row_connected[cid] == 1:
                if_row_connected[cid_merge] = 1
                num_row_connected[cid_merge] += num_row_connected[cid]
                num_row_connected[cid] = 0
                if_row_connected[cid2] = 0
            if if_connected_by_span[cid_merge] == 1 or if_connected_by_span[cid] == 1:
                if_connected_by_span[cid_merge] = 1
                num_connected_by_span[cid_merge] += num_connected_by_span[cid]
                num_connected_by_span[cid] = 0
                if_connected_by_span[cid] = 0
            num_connected_by_align[cid_merge] += num_connected_by_align[cid]
            num_connected_by_align[cid] = 0

            # code to merge columns for table
    prev_clusters = obj2cid2
    while True:
        for obj1, b1 in enumerate(boxes):
            cid1 = obj2cid2[obj1]
            rid1 = obj2rid[obj1]
            if cid1 in blacklist:
                continue
            if obj1 in blacklist_obj:
                continue
            for obj2, b2 in enumerate(boxes):
                if obj1 == obj2:
                    continue
                if obj2cid2[obj2] == cid1:
                    rid2 = obj2rid[obj2]
                    if rid1 == rid2:
                        continue
                    for obj3 in rid2obj[rid2]:
                        cid3 = obj2cid2[obj3]
                        if obj3 in blacklist_obj:
                            continue
                        if cid1 != cid3:
                            for obj4 in cid2obj2[cid3]:
                                if obj4 == obj3:
                                    continue
                                if obj2rid[obj4] == rid1:
                                    min_cid = min(cid1, cid3)
                                    max_cid = max(cid1, cid3)
                                    for obj_iter in cid2obj2[max_cid]:
                                        cid2obj2[min_cid].add(obj_iter)
                                        obj2cid2[obj_iter] = min_cid
                                    cid2obj2[max_cid] = set()
                                    # Features
                                    if_vertical_columns_merged[min_cid] = 1
                                    if_vertical_columns_merged[max_cid] = 0
                                    num_vertical_columns_merged[min_cid] += (
                                        num_vertical_columns_merged[max_cid]
                                    )
                                    num_vertical_columns_merged[max_cid] = 0
                                    if (
                                        if_row_connected[min_cid] == 1
                                        or if_row_connected[max_cid] == 1
                                    ):
                                        if_row_connected[min_cid] = 1
                                        num_row_connected[min_cid] += num_row_connected[
                                            max_cid
                                        ]
                                        num_row_connected[max_cid] = 0
                                        if_row_connected[max_cid] = 0
                                    if (
                                        if_connected_by_span[min_cid] == 1
                                        or if_connected_by_span[max_cid] == 1
                                    ):
                                        if_connected_by_span[min_cid] = 1
                                        num_connected_by_span[min_cid] += (
                                            num_connected_by_span[max_cid]
                                        )
                                        num_connected_by_span[max_cid] = 0
                                        if_connected_by_span[max_cid] = 0
                                    if (
                                        if_connected_by_align[min_cid] == 1
                                        or if_connected_by_align[max_cid] == 1
                                    ):
                                        if_connected_by_align[min_cid] = 1
                                        num_connected_by_align[min_cid] += (
                                            num_connected_by_align[max_cid]
                                        )
                                        num_connected_by_align[max_cid] = 0
                                        if_connected_by_align[max_cid] = 0
                                    break
        if prev_clusters == obj2cid2:
            break
        prev_clusters = obj2cid2

    clusters = [[boxes[i] for i in cluster] for cluster in filter(bool, cid2obj2)]
    nodes = [Node(elems) for elems in clusters]
    node_indices = [i for i, x in enumerate(cid2obj2) if x]
    merge_indices = [i for i in range(len(node_indices))]
    page_stat = Node(boxes)
    nodes, merge_indices = merge_nodes(nodes, plane, page_stat, merge_indices)
    # Features
    for idx in range(len(merge_indices)):
        if merge_indices[idx] != idx:
            cid1 = node_indices[merge_indices[idx]]
            cid2 = node_indices[idx]
            if if_row_connected[cid1] == 1 or if_row_connected[cid2] == 1:
                if_row_connected[cid1] = 1
                num_row_connected[cid1] += num_row_connected[cid2]
                num_row_connected[cid2] = 0
                if_row_connected[cid2] = 0
            if if_connected_by_span[cid1] == 1 or if_connected_by_span[cid2] == 1:
                if_connected_by_span[cid1] = 1
                num_connected_by_span[cid1] += num_connected_by_span[cid2]
                num_connected_by_span[cid2] = 0
                if_connected_by_span[cid2] = 0
            if if_connected_by_align[cid1] == 1 or if_connected_by_align[cid2] == 1:
                if_connected_by_align[cid1] = 1
                num_connected_by_align[cid1] += num_connected_by_align[cid2]
                num_connected_by_align[cid2] = 0
                if_connected_by_align[cid2] = 0
            if (
                if_vertical_columns_merged[cid1] == 1
                or if_vertical_columns_merged[cid2] == 1
            ):
                if_vertical_columns_merged[cid1] = 1
                num_vertical_columns_merged[cid1] += num_vertical_columns_merged[cid2]
                num_vertical_columns_merged[cid2] = 0
                if_vertical_columns_merged[cid2] = 0

    # Get Word Spacing Features
    rid2space = defaultdict(float)
    rid2space_norm = defaultdict(float)
    row_indices = [i for i, x in enumerate(rid2obj) if x]
    for rid in row_indices:
        obj_list = list(rid2obj[rid])
        if len(obj_list) == 1:
            rid2space[rid] = 0
            continue
        obj_boxes = [boxes[obj].bbox[0] for obj in obj_list]
        sorted_obj_idx = [
            i[0] for i in sorted(enumerate(obj_boxes), key=lambda x: x[1])
        ]
        for obj_idx in range(len(sorted_obj_idx) - 1):
            rid2space[rid] += (
                boxes[obj_list[sorted_obj_idx[obj_idx + 1]]].bbox[2]
                - boxes[obj_list[sorted_obj_idx[obj_idx]]].bbox[0]
            )
        rid2space_norm[rid] = rid2space[rid] / (len(obj_list) - 1)

    for idx, node in enumerate(nodes):
        node_idx = node_indices[idx]
        if merge_indices[idx] == idx:
            obj_list = []
            for idx_iter in range(len(merge_indices)):
                if merge_indices[idx_iter] == idx:
                    obj_list += list(cid2obj2[node_indices[idx_iter]])
            obj_list = list(set(obj_list))
            rid_list = list(set([obj2rid[obj] for obj in obj_list]))
            for rid in rid_list:
                total_word_space[node_idx] += rid2space[rid]
                avg_word_space_norm[node_idx] += rid2space_norm[rid]
                obj_boxes = [
                    boxes[obj].bbox[0] for obj in rid2obj if obj in cid2obj2[node_idx]
                ]
                sorted_obj_idx = [
                    i[0] for i in sorted(enumerate(obj_boxes), key=lambda x: x[1])
                ]
                for obj_idx in range(len(sorted_obj_idx) - 1):
                    node_space[node_idx] += (
                        boxes[obj_list[sorted_obj_idx[obj_idx + 1]]].bbox[2]
                        - boxes[obj_list[sorted_obj_idx[obj_idx]]].bbox[0]
                    )
                avg_node_space_norm[node_idx] += node_space[node_idx] / (
                    len(obj_boxes) - 1
                )
            avg_word_space[node_idx] = total_word_space[node_idx] / len(rid_list)
            avg_word_space_norm[node_idx] /= len(rid_list)
            avg_node_space[node_idx] = node_space[node_idx] / len(rid_list)
            avg_node_space_norm[node_idx] /= len(rid_list)

    new_nodes = []
    new_node_indices = []
    for idx in range(len(merge_indices)):
        if merge_indices[idx] == idx:
            new_nodes.append(nodes[idx])
            new_node_indices.append(node_indices[idx])

    nodes = new_nodes
    node_indices = new_node_indices
    # Features
    for idx, node in enumerate(nodes):
        node_idx = node_indices[idx]
        node_bbox = (node.x0, node.y0, node.x1, node.y1)
        for i1, b1 in enumerate(boxes_segments):
            if intersect(node_bbox, b1.bbox):
                num_segments[node_idx] += 1
        for i1, b1 in enumerate(boxes_figures):
            if intersect(node_bbox, b1.bbox):
                num_figures[node_idx] += 1
        for i1, b1 in enumerate(boxes_curves):
            if intersect(node_bbox, b1.bbox):
                num_curves[node_idx] += 1

    tables = []
    table_indices = []
    for idx, node in enumerate(nodes):
        node_idx = node_indices[idx]
        isTable = True
        if node.is_table():
            for elem in node.elems:
                if "table" in elem.get_text().lower():
                    continue
                if (node.width - elem.bbox[2] + elem.bbox[0]) < 2 * char_width:
                    isTable = False
            if isTable:
                tables.append(node)
                table_indices.append(node_idx)

    if combine == True:
        node_features = [0] * 17
        for idx, node in enumerate(nodes):
            node_idx = node_indices[idx]
            node_features = [
                sum(x)
                for x in zip(
                    node_features,
                    [
                        if_row_connected[node_idx],
                        num_row_connected[node_idx],
                        if_connected_by_span[node_idx],
                        num_connected_by_span[node_idx],
                        if_connected_by_align[node_idx],
                        num_connected_by_align[node_idx],
                        if_vertical_columns_merged[node_idx],
                        num_vertical_columns_merged[node_idx],
                        num_segments[node_idx],
                        num_curves[node_idx],
                        num_figures[node_idx],
                        total_word_space[node_idx],
                        avg_word_space[node_idx],
                        avg_word_space_norm[node_idx],
                        node_space[node_idx],
                        avg_node_space[node_idx],
                        avg_node_space_norm[node_idx],
                    ],
                )
            ]
        return [], node_features
    else:
        table_features = []
        for idx, table in enumerate(tables):
            table_idx = table_indices[idx]
            table_features += [
                [
                    if_row_connected[table_idx],
                    num_row_connected[table_idx],
                    if_connected_by_span[table_idx],
                    num_connected_by_span[table_idx],
                    if_connected_by_align[table_idx],
                    num_connected_by_align[table_idx],
                    if_vertical_columns_merged[table_idx],
                    num_vertical_columns_merged[table_idx],
                    num_segments[table_idx],
                    num_curves[table_idx],
                    num_figures[table_idx],
                    total_word_space[table_idx],
                    avg_word_space[table_idx],
                    avg_word_space_norm[table_idx],
                    node_space[table_idx],
                    avg_node_space[table_idx],
                    avg_node_space_norm[table_idx],
                ]
            ]
        return tables, table_features