"a7iv") only when both pieces are model-number sized. function spotfind_tokens( string $text ): array { $t = preg_split( '/[^a-z0-9]+/', spotfind_fold( $text ), -1, PREG_SPLIT_NO_EMPTY ); $out = $t; for ( $i = 0, $n = count( $t ) - 1; $i < $n; $i++ ) { if ( strlen( $t[ $i ] ) <= 3 && strlen( $t[ $i + 1 ] ) <= 3 ) { $out[] = $t[ $i ] . $t[ $i + 1 ]; } } return $out; } // Groups of single words -> term => other words of its groups. Multi-word or non a-z0-9 entries are ignored. function spotfind_synonym_map( array $groups ): array { $map = array(); foreach ( $groups as $g ) { $w = array_values( array_unique( array_filter( array_map( static fn( $s ) => strtolower( trim( (string) $s ) ), $g ), static fn( $s ) => preg_match( '/^[a-z0-9]+$/', $s ) ) ) ); foreach ( $w as $t ) { $map[ 'k' . $t ] = array_merge( $map[ 'k' . $t ] ?? array(), array_values( array_diff( $w, array( $t ) ) ) ); } } return $map; } /** * @param array[] $products each: id, n (name), p (price), img, u (url), s (in stock), b[] brands, c[] categories, sku, sales * @param array[] $synonyms groups of words, e.g. [["tv","television"]] * @param array[] $terms category and brand rows: ["c"|"b", name, url, count] * @return array{index: array, cards: array[]} * Products are stored most popular first (sales desc, then input order). Posting value = doc * 8 + field, ascending per term, * delta coded. Fields: 0 name, 1 brand, 2 category, 3 sku, 4 name after "for/fits/compatible". */ function spotfind_build( array $products, array $synonyms = array(), array $terms = array() ): array { $syn = spotfind_synonym_map( $synonyms ); foreach ( array_values( $products ) as $i => $p ) { $products[ $i ]['_i'] = $i; } $products = array_values( $products ); usort( $products, static fn( $a, $b ) => ( ( $b['sales'] ?? 0 ) <=> ( $a['sales'] ?? 0 ) ) ?: ( $a['_i'] <=> $b['_i'] ) ); // explicit tie-break: usort is not stable before PHP 8 $post = array(); $cards = array(); $ids = array(); $stock = ''; foreach ( $products as $i => $p ) { $name = html_entity_decode( $p['n'], ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $m = array(); $has = (bool) preg_match( '/^(.*?)\s+(?:for|fits|compatible)\s+(.*)$/isu', $name, $m ); // /u: \s must match a non-breaking space like JS does $list = array( array( 0, $has ? $m[1] : $name ), array( 1, implode( ' ', $p['b'] ?? array() ) ), array( 2, implode( ' ', $p['c'] ?? array() ) ), array( 3, $p['sku'] ?? '' ), ); if ( $has ) { $list[] = array( 4, $m[2] ); } foreach ( $list as list( $f, $txt ) ) { $t = spotfind_tokens( $txt ); foreach ( $t as $w ) { array_push( $t, ...( $syn[ 'k' . $w ] ?? array() ) ); } foreach ( array_unique( $t ) as $x ) { $post[ 'k' . $x ][] = $i * 8 + $f; // prefix: PHP turns numeric string keys into ints } } $ids[] = $p['id']; $stock .= empty( $p['s'] ) ? '0' : '1'; $reg = (float) ( $p['r'] ?? 0 ); $cards[ intdiv( $i, SPOTFIND_CHUNK ) ][] = array( $name, $p['p'], $p['img'] ?? '', $p['u'], $reg > (float) $p['p'] ? $reg : 0 ); // 5th: regular price, only when on sale } ksort( $post, SORT_STRING ); $delta = static function ( array $a ): array { $out = array(); foreach ( $a as $i => $x ) { $out[] = $i ? $x - $a[ $i - 1 ] : $x; } return $out; }; return array( 'index' => array( 'v' => 3, 'n' => count( $products ), 'pop' => array_filter( array_column( $products, 'sales' ), static fn( $s ) => $s > 0 ) ? 1 : 0, 'ids' => $ids, 's' => $stock, 't' => $terms, 'vocab' => array_map( static fn( $k ) => substr( $k, 1 ), array_keys( $post ) ), 'post' => array_map( $delta, array_values( $post ) ), ), 'cards' => array_values( $cards ), ); }