When you create your AWS account, Amazon automagically creates a default VPC in each region that spans the region. It also creates default subnets for you in each region; each subnet spans a single zone. (Note the plural "subnets".) There is no way for a subnet to span multiple zones.
When creating a spot request, you have the option of specifying a VPC and a subnet. If you want your spot request to only be for a specific zone, pick the default-generated subnet for that zone. (This addresses Michael's comment above.) That works just fine, and it's how I expected things to work.
However, you also have the option of "no preference" on the subnet (and the implied zone). I understood "no preference" to mean "any subnet". It looks like that's not the case (but I would welcome a correction here), and that "no preference" means Amazon picks a subnet for you from among the default subnets. That is, they pick a zone for you (as the zone is implied in the subnet they choose). It appears as though they pick the then-current low-price zone, but of course that won't always be the low-price zone for all time. And once the zone is chosen, it's fixed for the life of the request. (For a persistent request, that's forever.)
FWIW, another possibility is to create an auto-scaling group, and have it use spot instances instead of on-demand ones. With an auto-scaling group, you can have the group span multiple zones. However, it seems that the group always tries to spread requests across all the zones evenly regardless of price. It seems there is not an option for the group to place requests in the lowest price zone. But I s'pose that's a topic for another question ...
Update: I didn't use an auto-scaling group at first because it's more complicated than just creating a bunch of spot requests. But, of course, that didn't work either. It turns out the answer is: the only way to get the lowest-price zone is to use a spot fleet request.
I didn't do that at first either because it's more complicated to create, it requires the IAM user creating the fleet to have (slightly) more permissions than just
ec2:* actions, and the interface is still a bit wonky. See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-fleet-requests.html to get started. However, beware that this is still beta, and the documentation isn't up to date. Permissions also require "iam:GetRole", "iam:CreateRole", "iam:AttachRolePolicy". (The console will warn you about that when you try to create your first spot fleet. Apparently you still need these permissions even if the correct role already exists ... go figure.)
Back to my original problem: when creating the spot fleet, pick the "lowest price" strategy (which is not the default as the docs say, but it is available). Then -- as with auto-scaling group or spot request -- you must "select" the zones and check off them all (otherwise just the default zone is again used).
Hope someone finds this useful.