A number X is a possible gcd of two numbers in the range if there's two multiples of X in the range.
So we need to find the largest X such that there's m such that mX >= lower_bound and (m+1)X <= upper_bound.
For a given m, the largest X that satisfies this (if any) is X = floor(upper_bound / (m+1)), if floor(upper_bound/(m+1)) * m >= lower_bound. Note that a larger m will give a smaller X, so we need to find the smallest possible m to find the largest X.
Now we can just try m=1, m=2, and so on until we find an X (which will be largest possible X).
Note that we've sneakily avoided anything to do with gcd: if we find the largest X such that there's two multiples of X in the range, then necessarily those two multiples of X must have gcd X. And we can find the largest such X by finding the smallest m such that mX and (m+1)X are both in the range (and picking the largest such X for that m).
For example:
def findX(lower, upper):
for m in range(1, lower+1):
x = upper // (m + 1)
if x * m >= lower:
return 'input:%d,%d => %d, obtained with X=%d, Y=%d' % (lower, upper, x, x * m, x * (m+1))
print(findX(3, 4))
print(findX(10000, 10010))
print(findX(50000, 99999))
print(findX(50000, 100000))
Output:
input:3,4 => 1, obtained with X=3, Y=4
input:10000,10010 => 10, obtained with X=10000, Y=10010
input:50000,99999 => 33333, obtained with X=66666, Y=99999
input:50000,100000 => 50000, obtained with X=50000, Y=100000
(note: an earlier edit of this answer used binary search to find m and x. That was wrong because the existence of x for a particular m is not monotonic).