equal ignore case
diff --git a/spanner/key_recipe.go b/spanner/key_recipe.go
index a9e2d44..431abb7 100644
--- a/spanner/key_recipe.go
+++ b/spanner/key_recipe.go
@@ -24,6 +24,7 @@
 	"math"
 	"math/rand"
 	"strconv"
+	"strings"
 	"time"
 
 	sppb "cloud.google.com/go/spanner/apiv1/spannerpb"
@@ -554,11 +555,34 @@
 	if in != nil {
 		fields = in.GetFields()
 	}
+	var foldIndex map[string]string
+	foldIndexBuilt := false
 	return r.encodeKeyInternal(func(index int, identifier string) (*structpb.Value, valueLookupStatus) {
 		if identifier == "" {
 			return nil, valueLookupMissing
 		}
 		value, ok := fields[identifier]
+		if ok {
+			return value, valueLookupFound
+		}
+		if !foldIndexBuilt {
+			foldIndex = make(map[string]string, len(fields))
+			for fieldName := range fields {
+				foldedName := strings.ToLower(fieldName)
+				if existing, exists := foldIndex[foldedName]; !exists {
+					foldIndex[foldedName] = fieldName
+				} else if existing != fieldName {
+					// Multiple parameters differ only in case; treat as ambiguous.
+					foldIndex[foldedName] = ""
+				}
+			}
+			foldIndexBuilt = true
+		}
+		fallbackName, exists := foldIndex[strings.ToLower(identifier)]
+		if !exists || fallbackName == "" {
+			return nil, valueLookupMissing
+		}
+		value, ok = fields[fallbackName]
 		if !ok {
 			return nil, valueLookupMissing
 		}
diff --git a/spanner/key_recipe_test.go b/spanner/key_recipe_test.go
index 2ab82e9..c80311c 100644
--- a/spanner/key_recipe_test.go
+++ b/spanner/key_recipe_test.go
@@ -172,6 +172,73 @@
 	}
 }
 
+func TestKeyRecipe_QueryParamsCaseInsensitiveFallback(t *testing.T) {
+	recipeProto := parseKeyRecipeTextProto(t,
+		"part { tag: 1 }\n"+
+			"part {\n"+
+			"  order: ASCENDING\n"+
+			"  null_order: NULLS_FIRST\n"+
+			"  type { code: STRING }\n"+
+			"  identifier: \"id\"\n"+
+			"}\n")
+	params := parseStructTextProto(t,
+		"fields {\n"+
+			"  key: \"Id\"\n"+
+			"  value { string_value: \"foo\" }\n"+
+			"}\n")
+
+	recipe, err := newKeyRecipe(recipeProto)
+	if err != nil {
+		t.Fatalf("newKeyRecipe() failed: %v", err)
+	}
+	target := recipe.queryParamsToTargetRange(params)
+
+	wantStart := expectedKeyForStringValue(t, "foo")
+	if !bytes.Equal(target.start, wantStart) {
+		t.Fatalf("unexpected start with case-insensitive fallback: got %q want %q", target.start, wantStart)
+	}
+	if target.approximate {
+		t.Fatal("expected exact routing key with case-insensitive fallback")
+	}
+}
+
+func TestKeyRecipe_QueryParamsCaseInsensitiveFallbackAmbiguous(t *testing.T) {
+	recipeProto := parseKeyRecipeTextProto(t,
+		"part { tag: 1 }\n"+
+			"part {\n"+
+			"  order: ASCENDING\n"+
+			"  null_order: NULLS_FIRST\n"+
+			"  type { code: STRING }\n"+
+			"  identifier: \"ID\"\n"+
+			"}\n")
+	params := parseStructTextProto(t,
+		"fields {\n"+
+			"  key: \"Id\"\n"+
+			"  value { string_value: \"foo\" }\n"+
+			"}\n"+
+			"fields {\n"+
+			"  key: \"id\"\n"+
+			"  value { string_value: \"bar\" }\n"+
+			"}\n")
+
+	recipe, err := newKeyRecipe(recipeProto)
+	if err != nil {
+		t.Fatalf("newKeyRecipe() failed: %v", err)
+	}
+	target := recipe.queryParamsToTargetRange(params)
+
+	wantStart, err := appendCompositeTag(nil, 1)
+	if err != nil {
+		t.Fatalf("appendCompositeTag() failed: %v", err)
+	}
+	if !bytes.Equal(target.start, wantStart) {
+		t.Fatalf("unexpected start for ambiguous case-insensitive lookup: got %q want %q", target.start, wantStart)
+	}
+	if !target.approximate {
+		t.Fatal("expected approximate=true for ambiguous case-insensitive lookup")
+	}
+}
+
 func parseKeyRecipeTextProto(t *testing.T, text string) *sppb.KeyRecipe {
 	t.Helper()
 	recipe := &sppb.KeyRecipe{}