Commit e15d48cea6a for woocommerce

commit e15d48cea6aad4c45766ecd80fb8b7a932ac6964
Author: Brandon Kraft <public@brandonkraft.com>
Date:   Fri Jun 26 16:04:52 2026 -0500

    Fix tax state code autocomplete to allow searching by state code (#65882)

    * Fix tax state code autocomplete to allow searching by state code

    The Tax > Standard rates state field matched only the state name, so
    typing a state code (e.g. CA, TX, BD-05) did not surface the matching
    state. Generalize the country code-aware autocomplete source into a
    shared factory, apply it to the state field, and lower the state
    autocomplete minLength from 3 to 2 so two-letter codes can trigger.

    * Coerce autocomplete value to string before lowercasing

    Some state codes are numeric (e.g. US Minor Outlying Islands, where
    PHP integer-coerces the array keys), so they arrive in data.states as
    numbers. Calling .toLowerCase() on them threw a TypeError that broke
    the state autocomplete for any term matching such an entry. Stringify
    value and label before comparing.

    * Scope state autocomplete to the selected country

    The state field listed every country's states, so a code like AR
    surfaced Appenzell Ausserrhoden (CH), Arunachal Pradesh (IN), and
    Arezzo (IT) above Arkansas on a US row. Carry the country code through
    to data.states and, when the row's country is known, limit suggestions
    to that country's states. Falls back to all states when no or unknown
    country is entered so the field is never empty (e.g. state typed before
    country).

    * fix: set explicit html_entity_decode flags for PHP 8.1 compatibility

    ---------

    Co-authored-by: Sam Najian <dev@najian.info>

diff --git a/plugins/woocommerce/changelog/65881-state-code b/plugins/woocommerce/changelog/65881-state-code
new file mode 100644
index 00000000000..b851447c68c
--- /dev/null
+++ b/plugins/woocommerce/changelog/65881-state-code
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix tax state code autocomplete: allow searching by state code and scope suggestions to the selected country
diff --git a/plugins/woocommerce/client/legacy/js/admin/settings-views-html-settings-tax.js b/plugins/woocommerce/client/legacy/js/admin/settings-views-html-settings-tax.js
index 750271d85cb..60e139676fc 100644
--- a/plugins/woocommerce/client/legacy/js/admin/settings-views-html-settings-tax.js
+++ b/plugins/woocommerce/client/legacy/js/admin/settings-views-html-settings-tax.js
@@ -21,16 +21,29 @@
 			$pagination        = $( '#rates-pagination, #rates-bottom-pagination' ),
 			$search_field      = $( '#rates-search .wc-tax-rates-search-field' ),
 			$submit            = $( '.woocommerce-save-button[type=submit]' ),
-			countryAutocompleteSource = function( request, response ) {
+			/**
+			 * Filter a list of { value, label } items against the autocomplete term,
+			 * matching both the stored code (`value`) and the display name (`label`)
+			 * so a field holding a code can be found by typing the code, not just the
+			 * name. Matches are ranked so the most relevant code matches surface first.
+			 *
+			 * @param {Array}    items    List of { value, label } objects to search.
+			 * @param {Object}   request  jQuery UI autocomplete request ({ term }).
+			 * @param {Function} response jQuery UI autocomplete response callback.
+			 */
+			rankCodeAwareMatches = function( items, request, response ) {
 				var term    = request.term.toLowerCase(),
 					matcher = new RegExp( $.ui.autocomplete.escapeRegex( term ), 'i' ),
-					matches = $.grep( data.countries, function( country ) {
-						return matcher.test( country.value ) || matcher.test( country.label );
+					matches = $.grep( items, function( item ) {
+						return matcher.test( item.value ) || matcher.test( item.label );
 					} );

-				response( _.sortBy( matches, function( country ) {
-					var value = country.value.toLowerCase(),
-						label = country.label.toLowerCase();
+				/* Rank: exact code (0), code prefix (1), name prefix (2), name substring (3). */
+				response( _.sortBy( matches, function( item ) {
+					// Some state codes are numeric (e.g. US Minor Outlying Islands),
+					// which arrive as numbers, so coerce to string before comparing.
+					var value = String( item.value ).toLowerCase(),
+						label = String( item.label ).toLowerCase();

 					if ( value === term ) {
 						return 0;
@@ -44,6 +57,27 @@
 					return 3;
 				} ) );
 			},
+			countryAutocompleteSource = function( request, response ) {
+				rankCodeAwareMatches( data.countries, request, response );
+			},
+			stateAutocompleteSource = function( request, response ) {
+				// Scope state suggestions to the row's selected country when known,
+				// falling back to all states if no/unknown country is entered yet.
+				var country = String( this.element.closest( 'tr' ).find( 'td.country input' ).val() || '' ).trim().toUpperCase(),
+					states  = data.states;
+
+				if ( country ) {
+					var scoped = $.grep( data.states, function( state ) {
+						return state.country === country;
+					} );
+
+					if ( scoped.length ) {
+						states = scoped;
+					}
+				}
+
+				rankCodeAwareMatches( states, request, response );
+			},
 			WCTaxTableModelConstructor = Backbone.Model.extend({
 				changes: {},
 				setRateAttribute: function( rateID, attribute, value ) {
@@ -183,8 +217,8 @@

 					// Initialize autocomplete for states.
 					this.$el.find( 'td.state input' ).autocomplete({
-						source: data.states,
-						minLength: 3
+						source: stateAutocompleteSource,
+						minLength: 2
 					});

 					// Postcode and city don't have `name` values by default.
diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-tax.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-tax.php
index 662c3305141..4b78ff09885 100644
--- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-tax.php
+++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-tax.php
@@ -197,11 +197,12 @@ class WC_Settings_Tax extends WC_Settings_Page {
 		}

 		$states = array();
-		foreach ( WC()->countries->get_allowed_country_states() as $label ) {
-			foreach ( $label as $code => $state ) {
+		foreach ( WC()->countries->get_allowed_country_states() as $country_code => $country_states ) {
+			foreach ( $country_states as $code => $state ) {
 				$states[] = array(
-					'value' => $code,
-					'label' => esc_js( html_entity_decode( $state ) ),
+					'value'   => $code,
+					'label'   => esc_js( html_entity_decode( $state, ENT_QUOTES ) ),
+					'country' => $country_code,
 				);
 			}
 		}