, tri?: array} $ngrams1 * @param array{bi?: array, tri?: array} $ngrams2 * @return float Similarity score 0.0 to 1.0 */ public function diceCoefficient($ngrams1, $ngrams2) { $set1 = array_merge( isset($ngrams1['bi']) ? $ngrams1['bi'] : [], isset($ngrams1['tri']) ? $ngrams1['tri'] : [] ); $set2 = array_merge( isset($ngrams2['bi']) ? $ngrams2['bi'] : [], isset($ngrams2['tri']) ? $ngrams2['tri'] : [] ); if (empty($set1) || empty($set2)) { return 0.0; } $set1 = array_flip($set1); $set2 = array_flip($set2); $intersection = count(array_intersect_key($set1, $set2)); // Defensive: empty() check above should guarantee denominator > 0. $denominator = count($set1) + count($set2); return ($denominator > 0) ? (2.0 * $intersection) / $denominator : 0.0; } /** * Merge two arrays sorted by proximity to target, interleaving results. * * Both input arrays must be pre-sorted by proximity to the target: * - $below: ngram_count <= target, ordered DESC by ngram_count (closest first) * - $above: ngram_count > target, ordered ASC by ngram_count (closest first) * * @param array $below * @param array $above * @param int $targetNgramCount * @param int $limit * @return array */ public function mergeByProximity($below, $above, $targetNgramCount, $limit) { $result = []; $i = 0; $j = 0; $belowCount = count($below); $aboveCount = count($above); while (count($result) < $limit && ($i < $belowCount || $j < $aboveCount)) { $belowEntry = $below[$i] ?? null; $aboveEntry = $above[$j] ?? null; $belowNgramRaw = (is_array($belowEntry) && isset($belowEntry['ngram_count'])) ? $belowEntry['ngram_count'] : 0; $belowNgramCount = is_scalar($belowNgramRaw) ? (int)$belowNgramRaw : 0; $aboveNgramRaw = (is_array($aboveEntry) && isset($aboveEntry['ngram_count'])) ? $aboveEntry['ngram_count'] : 0; $aboveNgramCount = is_scalar($aboveNgramRaw) ? (int)$aboveNgramRaw : 0; $distBelow = ($i < $belowCount) ? abs($belowNgramCount - $targetNgramCount) : PHP_INT_MAX; $distAbove = ($j < $aboveCount) ? abs($aboveNgramCount - $targetNgramCount) : PHP_INT_MAX; // Prefer below on tie (includes exact matches) if ($distBelow <= $distAbove) { $result[] = $below[$i]; $i++; } else { $result[] = $above[$j]; $j++; } } return $result; } }