@@ -4110,7 +4110,7 @@ SZ_INTERNAL unsigned sz_utf8_case_rune_safety_profiles_mask_( //
41104110 sz_bool_t at_start = (prev_rune == 0 ) ? sz_true_k : sz_false_k ;
41114111 sz_bool_t at_end = (next_rune == 0 ) ? sz_true_k : sz_false_k ;
41124112
4113- // Helper: position detection for 's' rule (mid-ß-expansion avoidance)
4113+ // Helper: position detection for 's' rule (mid-ß-expansion avoidance in Western profile )
41144114 // Position 1: prev exists but prev_prev doesn't (prev is at position 0)
41154115 // Position N-2: next exists but next_next doesn't (next is at position N-1)
41164116 sz_bool_t at_pos_1 = (prev_rune != 0 && prev_prev_rune == 0 ) ? sz_true_k : sz_false_k ;
@@ -4183,12 +4183,21 @@ SZ_INTERNAL unsigned sz_utf8_case_rune_safety_profiles_mask_( //
41834183 break ;
41844184
41854185 // 'n':
4186- // - All : Contextual. Can't be first; can't follow 'ʼ' (U+02BC).
4186+ // - ASCII/Cyrillic/Greek : Contextual. Can't be first; can't follow 'ʼ' (U+02BC).
41874187 // Avoids: 'ʼn' (U+0149) → "ʼn".
4188+ // - Armenian: UNSAFE. Armenian kernel cannot handle ʼn (C5 89) → ʼn expansion.
4189+ // The character 'n' can match the 2nd part of the expansion, causing false positives.
4190+ // - Western/Central/Viet: Contextual, same as above.
41884191 // Western profile does NOT detect this in haystack scan.
41894192 case 'n' :
4190- if (at_start == sz_false_k && prev_ascii )
4191- safety |= strict_ascii_group | central_viet_group | western_group ;
4193+ // Exclude Armenian - it cannot handle ʼn → ʼn one-to-many expansion
4194+ if (at_start == sz_false_k && prev_ascii ) {
4195+ safety |= (1 << sz_utf8_case_rune_safe_ascii_k ) | //
4196+ (1 << sz_utf8_case_rune_safe_cyrillic_k ) | //
4197+ (1 << sz_utf8_case_rune_safe_greek_k ); //
4198+ // Armenian EXCLUDED: sz_utf8_case_rune_safe_armenian_k
4199+ safety |= central_viet_group | western_group ;
4200+ }
41924201 break ;
41934202
41944203 // 'i':
@@ -4235,8 +4244,8 @@ SZ_INTERNAL unsigned sz_utf8_case_rune_safety_profiles_mask_( //
42354244
42364245 // 's'
42374246 // - Strict: UNSAFE. 'ſ' (U+017F) folds to 's'.
4238- // - Central/Viet : Contextual. Can't be first/last; can't be adjacent to 's'/'t'.
4239- // Avoids: "ss" (Eszett expansion) and "st" (ligature expansion).
4247+ // - Central/Vietnamese : Contextual. Can't be first/last; can't be adjacent to 's'/'t'.
4248+ // Avoids: "ss" (Eszett expansion), "st" (ligature expansion), and ſ→s in danger zones .
42404249 // - Western: Contextual. Can't be at positions 0, 1 (if prev='s'), N-1, or N-2 (if next='s').
42414250 // Avoids mid-ß-expansion matches: ß→"ss" in-place means needle with 's' at these
42424251 // positions could match at byte offset 1 (UTF-8 continuation byte 0x9F).
@@ -4284,8 +4293,9 @@ SZ_INTERNAL unsigned sz_utf8_case_rune_safety_profiles_mask_( //
42844293 safety |= western_group ;
42854294 }
42864295 else if (rune == 0x00B5 ) {
4287- // 'µ' (Micro Sign) folds to 'μ' (Greek Mu, CE BC) - BANNED from Western Europe
4288- // to avoid Greek complexity. Needles with µ use serial fallback.
4296+ // 'µ' (Micro Sign) folds to Greek 'μ' (U+03BC, CE BC).
4297+ // Allow only the Greek SIMD path; Latin paths remain unsafe.
4298+ safety |= (1 << sz_utf8_case_rune_safe_greek_k );
42894299 }
42904300 else { safety |= western_group | central_viet_group ; }
42914301 }
@@ -4379,8 +4389,9 @@ SZ_INTERNAL unsigned sz_utf8_case_rune_safety_profiles_mask_( //
43794389 case 0x0535 : // U+0535 Ech uppercase
43804390 if (at_start || lower_prev_arm == 0x0574 || lower_next_arm == 0x0582 ) armenian_safe = sz_false_k ;
43814391 break ;
4382- case 0x0582 : // U+0582 yiwn - can't be last; can't follow U+0565 ech
4383- if (at_end || lower_prev_arm == 0x0565 ) armenian_safe = sz_false_k ;
4392+ case 0x0582 : // U+0582 yiwn - can't be first; can't be last; can't follow U+0565 ech
4393+ // Armenian ligature և (U+0587) → ech + yiwn; needle starting with yiwn matches mid-expansion
4394+ if (at_start || at_end || lower_prev_arm == 0x0565 ) armenian_safe = sz_false_k ;
43844395 break ;
43854396 case 0x0574 : // U+0574 men - can't be last; can't precede U+0576, U+0565, U+056B, U+056D
43864397 case 0x0544 : // U+0544 Men uppercase
@@ -5750,6 +5761,10 @@ SZ_INTERNAL __m512i sz_utf8_case_insensitive_find_ice_greek_fold_zmm_(__m512i te
57505761 // Lead bytes:
57515762 __m512i const x_ce_zmm = _mm512_set1_epi8 ((char )0xCE );
57525763 __m512i const x_cf_zmm = _mm512_set1_epi8 ((char )0xCF );
5764+ // Micro sign µ (C2 B5) folds to Greek μ (CE BC) in-place.
5765+ __m512i const x_c2_zmm = _mm512_set1_epi8 ((char )0xC2 );
5766+ __m512i const x_b5_zmm = _mm512_set1_epi8 ((char )0xB5 );
5767+ __m512i const x_bc_zmm = _mm512_set1_epi8 ((char )0xBC );
57535768
57545769 // Ranges for CE lead byte:
57555770 // CE 86-8F (Accented Upper): 'Ά' (86) ... 'Ώ' (8F)
@@ -5848,6 +5863,12 @@ SZ_INTERNAL __m512i sz_utf8_case_insensitive_find_ice_greek_fold_zmm_(__m512i te
58485863 // Apply Final Sigma
58495864 result_zmm = _mm512_mask_mov_epi8 (result_zmm , is_final_sigma , x_83_zmm );
58505865
5866+ // Apply Micro Sign folding: C2 B5 -> CE BC
5867+ __mmask64 is_c2_mask = _mm512_cmpeq_epi8_mask (result_zmm , x_c2_zmm );
5868+ __mmask64 is_micro_second = (is_c2_mask << 1 ) & _mm512_cmpeq_epi8_mask (result_zmm , x_b5_zmm );
5869+ result_zmm = _mm512_mask_mov_epi8 (result_zmm , is_micro_second >> 1 , x_ce_zmm ); // Lead C2 -> CE
5870+ result_zmm = _mm512_mask_mov_epi8 (result_zmm , is_micro_second , x_bc_zmm ); // Second B5 -> BC
5871+
58515872 return result_zmm ;
58525873}
58535874
@@ -5899,16 +5920,14 @@ SZ_INTERNAL sz_cptr_t sz_utf8_case_insensitive_find_ice_greek_( //
58995920 // - CE B0: 'ΰ' -> "ΰ" (3 bytes)
59005921 // - CF 90, 91, 95, 96: Greek Symbols 'ϐ', 'ϑ', 'ϕ', 'ϖ'
59015922 // - CF B0, B1, B5: Greek Symbols 'ϰ', 'ϱ', 'ϵ'
5902- // - C2 B5: Micro Sign 'µ' -> 'μ' (CE BC)
59035923 // - E2 84 A6: Ohm Sign 'Ω' -> 'ω' (CF 89) (detect E2 84 prefix)
59045924 // - E1 xx xx: Polytonic Greek (excluded by safety profile, but just in case)
5905- sz_u512_vec_t x_ce_vec , x_cf_vec , x_c2_vec , x_e1_vec , x_e2_vec , x_cd_vec ;
5925+ sz_u512_vec_t x_ce_vec , x_cf_vec , x_e1_vec , x_e2_vec , x_cd_vec ;
59065926 sz_u512_vec_t x_90_vec , x_b0_vec , x_84_vec ;
59075927 sz_u512_vec_t x_91_vec , x_95_vec , x_96_vec , x_b1_vec , x_b5_vec ;
59085928
59095929 x_ce_vec .zmm = _mm512_set1_epi8 ((char )0xCE );
59105930 x_cf_vec .zmm = _mm512_set1_epi8 ((char )0xCF );
5911- x_c2_vec .zmm = _mm512_set1_epi8 ((char )0xC2 );
59125931 x_e1_vec .zmm = _mm512_set1_epi8 ((char )0xE1 ); // E1 lead byte (Polytonic)
59135932 x_e2_vec .zmm = _mm512_set1_epi8 ((char )0xE2 );
59145933 x_cd_vec .zmm = _mm512_set1_epi8 ((char )0xCD ); // CD lead byte (Combining Diacritics)
@@ -5948,12 +5967,11 @@ SZ_INTERNAL sz_cptr_t sz_utf8_case_insensitive_find_ice_greek_( //
59485967 // Check for anomalies
59495968 __mmask64 x_ce_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_ce_vec .zmm );
59505969 __mmask64 x_cf_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_cf_vec .zmm );
5951- __mmask64 x_c2_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_c2_vec .zmm );
59525970 __mmask64 x_e2_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_e2_vec .zmm );
59535971 __mmask64 x_e1_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_e1_vec .zmm );
59545972 __mmask64 x_cd_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_cd_vec .zmm );
59555973
5956- if (x_ce_mask | x_cf_mask | x_c2_mask | x_e2_mask | x_e1_mask | x_cd_mask ) {
5974+ if (x_ce_mask | x_cf_mask | x_e2_mask | x_e1_mask | x_cd_mask ) {
59575975 __mmask64 x_90_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_90_vec .zmm );
59585976 __mmask64 x_b0_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_b0_vec .zmm );
59595977 __mmask64 x_be_ce_90_b0 = (x_ce_mask << 1 ) & (x_90_mask | x_b0_mask ); // CE 90 or CE B0
@@ -5969,11 +5987,10 @@ SZ_INTERNAL sz_cptr_t sz_utf8_case_insensitive_find_ice_greek_( //
59695987
59705988 __mmask64 x_be_cf_sym = (x_cf_mask << 1 ) & (x_9x_mask | x_bx_mask );
59715989
5972- __mmask64 x_micro = (x_c2_mask << 1 ) & x_bx_mask ; // C2 B5 (B5 is in bx_mask)
59735990 __mmask64 x_ohm = (x_e2_mask << 1 ) & _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_84_vec .zmm );
59745991 // E1 is always dangerous in Greek path (Polytonic/Extensions)
59755992 // CD is dangerous (Combining Diacritical Marks)
5976- __mmask64 danger_mask = x_be_ce_90_b0 | x_be_cf_sym | x_micro | x_ohm | x_e1_mask | x_cd_mask ;
5993+ __mmask64 danger_mask = x_be_ce_90_b0 | x_be_cf_sym | x_ohm | x_e1_mask | x_cd_mask ;
59775994
59785995 if (danger_mask ) {
59795996 sz_cptr_t match = sz_utf8_case_insensitive_find_in_danger_zone_ ( //
@@ -6176,9 +6193,11 @@ SZ_INTERNAL sz_cptr_t sz_utf8_case_insensitive_find_ice_vietnamese_( //
61766193 // 1E 9E (ẞ) is in the 96-9F range, so covered.
61776194 // EF AC 80-86: Ligatures (ff-st) that expand when folded.
61786195 // E2 84 AA: Kelvin sign K folds to 'k'.
6196+ // C5 BF: Long S 'ſ' folds to 's'.
61796197
6180- sz_u512_vec_t x_e1_vec , x_ba_vec , x_96_vec , x_9f_vec ;
6181- sz_u512_vec_t x_c3_vec , x_9f_byte_vec ;
6198+ sz_u512_vec_t x_e1_vec , x_ba_vec , x_96_vec ;
6199+ sz_u512_vec_t x_c3_vec , x_9f_vec ;
6200+ sz_u512_vec_t x_c5_vec , x_bf_vec ;
61826201 sz_u512_vec_t x_ef_vec , x_ac_vec ;
61836202 sz_u512_vec_t x_e2_vec , x_84_vec ;
61846203
@@ -6187,7 +6206,8 @@ SZ_INTERNAL sz_cptr_t sz_utf8_case_insensitive_find_ice_vietnamese_( //
61876206 x_96_vec .zmm = _mm512_set1_epi8 ((char )0x96 );
61886207 x_9f_vec .zmm = _mm512_set1_epi8 ((char )0x9F );
61896208 x_c3_vec .zmm = _mm512_set1_epi8 ((char )0xC3 );
6190- x_9f_byte_vec .zmm = _mm512_set1_epi8 ((char )0x9F );
6209+ x_c5_vec .zmm = _mm512_set1_epi8 ((char )0xC5 );
6210+ x_bf_vec .zmm = _mm512_set1_epi8 ((char )0xBF );
61916211 x_ef_vec .zmm = _mm512_set1_epi8 ((char )0xEF );
61926212 x_ac_vec .zmm = _mm512_set1_epi8 ((char )0xAC );
61936213 x_e2_vec .zmm = _mm512_set1_epi8 ((char )0xE2 );
@@ -6216,18 +6236,23 @@ SZ_INTERNAL sz_cptr_t sz_utf8_case_insensitive_find_ice_vietnamese_( //
62166236 // Check for anomalies
62176237 __mmask64 x_e1_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_e1_vec .zmm );
62186238 __mmask64 x_c3_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_c3_vec .zmm );
6239+ __mmask64 x_c5_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_c5_vec .zmm );
62196240 __mmask64 x_ef_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_ef_vec .zmm );
62206241 __mmask64 x_e2_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_e2_vec .zmm );
62216242
6222- if (x_e1_mask | x_c3_mask | x_ef_mask | x_e2_mask ) {
6243+ if (x_e1_mask | x_c3_mask | x_c5_mask | x_ef_mask | x_e2_mask ) {
62236244 // Check E1 BA 96-9F
62246245 __mmask64 x_ba_second = (x_e1_mask << 1 ) & _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_ba_vec .zmm );
62256246 __mmask64 x_bad_third = (x_ba_second << 1 ) &
62266247 _mm512_mask_cmpge_epu8_mask (load_mask , haystack_vec .zmm , x_96_vec .zmm ) &
62276248 _mm512_mask_cmple_epu8_mask (load_mask , haystack_vec .zmm , x_9f_vec .zmm );
62286249
62296250 // Check C3 9F (Sharp S)
6230- __mmask64 x_sharp_s = (x_c3_mask << 1 ) & _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_9f_byte_vec .zmm );
6251+ __mmask64 x_sharp_s = (x_c3_mask << 1 ) & _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_9f_vec .zmm );
6252+
6253+ // Check C5 BF (Long S)
6254+ __mmask64 x_bf_mask = _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_bf_vec .zmm );
6255+ __mmask64 x_long_s = (x_c5_mask << 1 ) & x_bf_mask ;
62316256
62326257 // Check EF AC (Ligatures: ff-st, U+FB00-FB06)
62336258 __mmask64 x_ligature = (x_ef_mask << 1 ) & _mm512_cmpeq_epi8_mask (haystack_vec .zmm , x_ac_vec .zmm );
@@ -6244,7 +6269,8 @@ SZ_INTERNAL sz_cptr_t sz_utf8_case_insensitive_find_ice_vietnamese_( //
62446269 // x_ligature marks 2nd byte. Shift right 1.
62456270 // x_kelvin marks 2nd byte. Shift right 1.
62466271
6247- __mmask64 danger_mask = (x_bad_third >> 2 ) | (x_sharp_s >> 1 ) | (x_ligature >> 1 ) | (x_kelvin >> 1 );
6272+ __mmask64 danger_mask =
6273+ (x_bad_third >> 2 ) | (x_sharp_s >> 1 ) | (x_long_s >> 1 ) | (x_ligature >> 1 ) | (x_kelvin >> 1 );
62486274
62496275 if (danger_mask ) {
62506276 sz_cptr_t match = sz_utf8_case_insensitive_find_in_danger_zone_ ( //
@@ -6332,6 +6358,7 @@ SZ_PUBLIC sz_cptr_t sz_utf8_case_insensitive_find_ice( //
63326358 sz_size_t longest = 0 ;
63336359 longest = sz_max_of_two (longest , analysis .ascii .length );
63346360 longest = sz_max_of_two (longest , analysis .western_europe .length );
6361+ longest = sz_max_of_two (longest , analysis .central_europe .length );
63356362 longest = sz_max_of_two (longest , analysis .cyrillic .length );
63366363 longest = sz_max_of_two (longest , analysis .greek .length );
63376364 longest = sz_max_of_two (longest , analysis .armenian .length );
@@ -6342,12 +6369,14 @@ SZ_PUBLIC sz_cptr_t sz_utf8_case_insensitive_find_ice( //
63426369 return sz_utf8_case_insensitive_find_serial ( //
63436370 haystack , haystack_length , needle , needle_length , needle_metadata , matched_length );
63446371
6345- // Select the best window (prefer ASCII > Western > others for tie-breaking)
6372+ // Select the best window (prefer ASCII > Western > Central > others for tie-breaking)
63466373 sz_utf8_string_slice_t const * best = 0 ;
63476374 if (analysis .ascii .length == longest )
63486375 best = & analysis .ascii , needle_metadata -> kernel_id = sz_utf8_case_rune_safe_ascii_k ;
63496376 else if (analysis .western_europe .length == longest )
63506377 best = & analysis .western_europe , needle_metadata -> kernel_id = sz_utf8_case_rune_safe_western_europe_k ;
6378+ else if (analysis .central_europe .length == longest )
6379+ best = & analysis .central_europe , needle_metadata -> kernel_id = sz_utf8_case_rune_safe_central_europe_k ;
63516380 else if (analysis .cyrillic .length == longest )
63526381 best = & analysis .cyrillic , needle_metadata -> kernel_id = sz_utf8_case_rune_safe_cyrillic_k ;
63536382 else if (analysis .greek .length == longest )
0 commit comments