Advent of code 2018/25
Ajax Direct

Answer 354ms

Part 1 : 314 Part 2 :
/**
 * Check if an item is close enough from a group to join it
 */
function is_close_enough($group, $a) {
    foreach ($group as $b) {
        $manhattan = abs($a[0] - $b[0]) + abs($a[1] - $b[1]) + abs($a[2] - $b[2]) + abs($a[3] - $b[3]);
        if ($manhattan <= 3) return true;
    }
    return false;
}


/**
 * If an item can merge one or more groups, merge all of them together.
 * Else create a new group.
 */
$items = (array) $input->lines->map(fn ($l) => explode(",", $l));
$groups = [];

foreach ($items as $item) {
    $can_merge = [];
    $new_group = [$item];

    foreach ($groups as $g=>$group) {
        if (is_close_enough($group, $item)) {
            $can_merge[] = $g;
        }
    }

    if (!empty($can_merge)) {
        foreach ($can_merge as $c) {
            $new_group = array_merge($new_group, $groups[$c]);
            unset($groups[$c]);
        }
    }

    $groups[] = $new_group;
}

$solution_1 = count($groups);
$solution_2 = "⭐";