@@ -1177,7 +1177,8 @@ SZ_INTERNAL sz_bool_t sz_utf8_case_insensitive_verify_head_(sz_cptr_t needle_sta
11771177 for (;;) {
11781178 sz_bool_t have_needle = sz_utf8_folded_reverse_iter_prev_ (& needle_riter , & needle_rune );
11791179
1180- // Needle exhausted - success!
1180+ // Needle exhausted - success! Unconsumed haystack runes are OK.
1181+ // Example: "fi" matches suffix of "ffi" (folds to "ffi"), leaving first 'f' unused.
11811182 if (!have_needle ) {
11821183 * match_length = (sz_size_t )(haystack_end - haystack_riter .ptr );
11831184 return sz_true_k ;
@@ -1510,10 +1511,10 @@ SZ_INTERNAL sz_cptr_t sz_utf8_case_insensitive_find_in_danger_zone_( //
15101511 if (needle_riter_rune != haystack_riter_rune ) goto consider_third_haystack_folded_rune ;
15111512 }
15121513
1513- // First match the tail
1514+ // First match the tail (from safe window start forward)
15141515 sz_rune_t needle_iter_rune = 0 , haystack_iter_rune = 0 ;
15151516 sz_utf8_folded_iter_t_ needle_iter , haystack_iter ;
1516- sz_utf8_folded_iter_init_ (& needle_iter , needle_ptr + needle_length - needle_first_safe_folded_rune_offset ,
1517+ sz_utf8_folded_iter_init_ (& needle_iter , needle_ptr + needle_first_safe_folded_rune_offset ,
15171518 needle_length - needle_first_safe_folded_rune_offset );
15181519 sz_utf8_folded_iter_init_ (& haystack_iter , danger_ptr + haystack_rune_length ,
15191520 haystack_end - (danger_ptr + haystack_rune_length ));
@@ -1591,10 +1592,10 @@ SZ_INTERNAL sz_cptr_t sz_utf8_case_insensitive_find_in_danger_zone_( //
15911592 if (needle_riter_rune != haystack_riter_rune ) goto consider_following_haystack_runes ;
15921593 }
15931594
1594- // First match the tail
1595+ // First match the tail (from safe window start forward)
15951596 sz_rune_t needle_iter_rune = 0 , haystack_iter_rune = 0 ;
15961597 sz_utf8_folded_iter_t_ needle_iter , haystack_iter ;
1597- sz_utf8_folded_iter_init_ (& needle_iter , needle_ptr + needle_length - needle_first_safe_folded_rune_offset ,
1598+ sz_utf8_folded_iter_init_ (& needle_iter , needle_ptr + needle_first_safe_folded_rune_offset ,
15981599 needle_length - needle_first_safe_folded_rune_offset );
15991600 sz_utf8_folded_iter_init_ (& haystack_iter , danger_ptr + haystack_rune_length ,
16001601 haystack_end - (danger_ptr + haystack_rune_length ));
@@ -1846,8 +1847,6 @@ SZ_PUBLIC sz_cptr_t sz_utf8_case_insensitive_find_serial( //
18461847 sz_rune_t needle_runes [32 ];
18471848 sz_size_t needle_prefix_count = 0 , needle_total_count = 0 ;
18481849 sz_u64_t needle_hash = 0 ;
1849- sz_cptr_t needle_tail = SZ_NULL_CHAR ;
1850- sz_size_t needle_tail_length = 0 ;
18511850 {
18521851 sz_utf8_folded_iter_t_ needle_iter ;
18531852 sz_utf8_folded_iter_init_ (& needle_iter , needle , needle_length );
@@ -1857,11 +1856,8 @@ SZ_PUBLIC sz_cptr_t sz_utf8_case_insensitive_find_serial( //
18571856 needle_hash = needle_hash * 257 + rune ;
18581857 }
18591858 needle_total_count = needle_prefix_count ;
1860- if (needle_prefix_count == ring_capacity ) {
1861- needle_tail = needle_iter .ptr ;
1862- while (sz_utf8_folded_iter_next_ (& needle_iter , & rune )) needle_total_count ++ ;
1863- needle_tail_length = needle_length - (sz_size_t )(needle_tail - needle );
1864- }
1859+ // For long needles, count remaining runes beyond ring buffer
1860+ while (sz_utf8_folded_iter_next_ (& needle_iter , & rune )) needle_total_count ++ ;
18651861 }
18661862 if (!needle_prefix_count ) {
18671863 * match_length = 0 ;
@@ -1872,7 +1868,7 @@ SZ_PUBLIC sz_cptr_t sz_utf8_case_insensitive_find_serial( //
18721868 for (sz_size_t i = 1 ; i < needle_prefix_count ; ++ i ) hash_multiplier *= 257 ;
18731869
18741870 sz_rune_t window_runes [32 ];
1875- sz_cptr_t window_sources [32 ];
1871+ sz_cptr_t window_sources [32 ]; // Byte position of character that produced each window rune
18761872 sz_size_t ring_head = 0 ;
18771873 sz_u64_t window_hash = 0 ;
18781874 sz_utf8_folded_iter_t_ haystack_iter ;
@@ -1887,7 +1883,8 @@ SZ_PUBLIC sz_cptr_t sz_utf8_case_insensitive_find_serial( //
18871883 sz_rune_t rune ;
18881884 if (!sz_utf8_folded_iter_next_ (& haystack_iter , & rune )) break ;
18891885 window_runes [window_count ] = rune ;
1890- current_source = haystack_iter .pending_idx <= 1 ? before_ptr : current_source ;
1886+ // Update source only when starting a new character (not mid-expansion)
1887+ if (haystack_iter .pending_idx <= 1 || haystack_iter .pending_count == 0 ) current_source = before_ptr ;
18911888 window_sources [window_count ] = current_source ;
18921889 window_hash = window_hash * 257 + rune ;
18931890 window_count ++ ;
@@ -1911,34 +1908,17 @@ SZ_PUBLIC sz_cptr_t sz_utf8_case_insensitive_find_serial( //
19111908 for (sz_size_t i = 0 ; i < ring_head ; ++ i ) mismatches += window_runes [i ] != needle_runes [first_segment + i ];
19121909
19131910 if (!mismatches ) {
1911+ // Short needle: rune comparison above is sufficient verification
19141912 if (needle_total_count <= ring_capacity ) {
1915- // Verify the match to handle edge cases where window_start includes partial
1916- // multi-rune expansions (e.g., ß→ss where only the second 's' is in the match)
1917- sz_size_t verified_match_length = 0 ;
1918- if (sz_utf8_case_insensitive_verify_tail_ (needle , needle + needle_length , window_start , window_end ,
1919- & verified_match_length )) {
1920- * match_length = verified_match_length ;
1921- return window_start ;
1922- }
1923- // Rune hash matched but byte-level verification failed, continue searching
1924- }
1925- else {
1926- sz_size_t haystack_tail_length = haystack_length - (sz_size_t )(window_end - haystack );
1927- sz_utf8_folded_iter_t_ needle_tail_iter , haystack_tail_iter ;
1928- sz_utf8_folded_iter_init_ (& needle_tail_iter , needle_tail , needle_tail_length );
1929- sz_utf8_folded_iter_init_ (& haystack_tail_iter , window_end , haystack_tail_length );
1930- sz_rune_t needle_tail_rune , haystack_tail_rune ;
1931- sz_bool_t tail_matches = sz_true_k ;
1932- while (tail_matches && sz_utf8_folded_iter_next_ (& needle_tail_iter , & needle_tail_rune )) {
1933- if (!sz_utf8_folded_iter_next_ (& haystack_tail_iter , & haystack_tail_rune ) ||
1934- needle_tail_rune != haystack_tail_rune )
1935- tail_matches = sz_false_k ;
1936- }
1937- if (tail_matches ) {
1938- * match_length = (sz_size_t )(haystack_tail_iter .ptr - window_start );
1939- return window_start ;
1940- }
1913+ * match_length = (sz_size_t )(window_end - window_start );
1914+ return window_start ;
19411915 }
1916+ // Long needle: verify FULL needle from window_start (not just tail from window_end).
1917+ // This handles mid-expansion matches correctly. Example: if window ends mid-way through
1918+ // ffi→"ffi", starting verification from window_start ensures proper rune alignment.
1919+ if (sz_utf8_case_insensitive_verify_tail_ (needle , needle + needle_length , window_start ,
1920+ haystack + haystack_length , match_length ))
1921+ return window_start ;
19421922 }
19431923 }
19441924
@@ -1954,7 +1934,8 @@ SZ_PUBLIC sz_cptr_t sz_utf8_case_insensitive_find_serial( //
19541934 next_head = next_head == needle_prefix_count ? 0 : next_head ;
19551935
19561936 window_runes [ring_head ] = new_rune ;
1957- current_source = haystack_iter .pending_idx <= 1 ? before_ptr : current_source ;
1937+ // Update source only when starting a new character (not mid-expansion)
1938+ if (haystack_iter .pending_idx <= 1 || haystack_iter .pending_count == 0 ) current_source = before_ptr ;
19581939 window_sources [ring_head ] = current_source ;
19591940
19601941 ring_head = next_head ;
0 commit comments