Divide the scene into buckets, either into N x-buckets and M y-buckets, or into N*M x*y buckets. In the former case, the buckets are stored in two arrays (an x-array and a y-array); in the latter case, the buckets are stored in an array of arrays (the outer arrays indexes the x-coordinates, the inner arrays index the y-coordinates). In either case, the buckets store references to all of the points within the area indexed by the bucket; for example, the point (8, 12) would be in the x-bucket [5, 10] and the y-bucket [10, 15], or else it would be in the x*y bucket ([5, 10], [10, 15]).
When looking up a point, either look up the appropriate x and y buckets, or else simply look up the appropriate x*y buckets. In the former case, take intersection(union(x-buckets), union(y-buckets))
. You may need to look up multiple buckets depending on the hit radius, for example if you're looking up the x-coordinate 9 with radius 2 then you'd need both the [5, 10] and [10, 15] buckets.
Using separate x and y buckets takes up less space (N + M buckets instead of N*M buckets) and makes the indexing easier (two separate arrays vs. one nested array), while the x*y buckets make for faster lookups since you won't need to take any set intersections.
The smaller your buckets, the more space the data structure will take up, but the fewer false positives you'll retrieve. Ideally, if you have sufficient memory, then the buckets will cover the same interval as the hit radius.