public void Lookup(IPAddress ip, Action <GeoInfo> callback)
        {
            uint    ipInt   = Ip4Utils.ToInt(ip);
            GeoInfo geoInfo = null;

            lock (cache)
            {
                if (cache.TryGetValue(ipInt, out var cached))
                {
                    geoInfo = cached as GeoInfo;
                }

                if (geoInfo == null)
                {
                    if (this.usageExceeded == DateTime.UtcNow.Date)
                    {
                        return;
                    }

                    if (cached == null)
                    {
                        cache.Add(ipInt, callback);
                        this.queue.Add(ip);
                    }
                    else
                    {
                        var callbacks = (Action <GeoInfo>)cached;
                        callbacks   += callback;
                        cache[ipInt] = callbacks;
                    }
                    return;
                }
            }
            callback(geoInfo);
        }
        public void LoadCache()
        {
            if (!File.Exists(this.cacheFile))
            {
                return;
            }
            lock (cache)
            {
                foreach (var line in File.ReadAllLines(this.cacheFile))
                {
                    try
                    {
                        var  parts  = line.Split(new[] { '=' }, 2);
                        uint ipInt  = 0;
                        var  octets = parts[0].Split('.');
                        foreach (var octet in octets)
                        {
                            ipInt = (ipInt << 8) + uint.Parse(octet);
                        }
                        var loc         = parts[1].Split('|');
                        var countryCode = loc[0];
                        var latitude    = decimal.Parse(loc[5], NumberFormatInfo.InvariantInfo);
                        var longitude   = decimal.Parse(loc[6], NumberFormatInfo.InvariantInfo);
                        if (countryCode != "")
                        {
                            var geoInfo = new GeoInfo(countryCode, loc[1], loc[2], loc[3], loc[4], latitude, longitude);
                            cache[ipInt] = geoInfo;
                        }
                    }
                    catch
                    {
                        // ignore
                    }
                }

                // override wrong geo-IP information (MS Azure IPs list Washington even for NL/EU servers)
                cache[Ip4Utils.ToInt(104, 40, 134, 97)]  = new GeoInfo("NL", "Netherlands", null, null, null, 0, 0);
                cache[Ip4Utils.ToInt(104, 40, 213, 215)] = new GeoInfo("NL", "Netherlands", null, null, null, 0, 0);

                // Vultr also spreads their IPs everywhere
                cache[Ip4Utils.ToInt(45, 32, 153, 115)] = new GeoInfo("DE", "Germany", null, null, null, 0, 0);       // listed as NL, but is DE
                cache[Ip4Utils.ToInt(45, 32, 205, 149)] = new GeoInfo("US", "United States", "TX", null, null, 0, 0); // listed as NL, but is TX

                // i3d.net
                cache[Ip4Utils.ToInt(185, 179, 200, 69)] = new GeoInfo("ZA", "South Africa", null, null, null, 0, 0); // listed as NL, but is ZA
            }
        }
        private GeoInfo HandleResult(uint ip, string result)
        {
            var ser  = new DataContractJsonSerializer(typeof(NekudoGeopIpFullResponse));
            var info = (NekudoGeopIpFullResponse)ser.ReadObject(new MemoryStream(Encoding.UTF8.GetBytes(result)));

            var subdiv  = info.subdivisions?[info.subdivisions.Length - 1];
            var geoInfo = new GeoInfo(
                info.country?.iso_code, TryGet(info.country?.names, "en"),
                subdiv?.iso_code, TryGet(subdiv?.names, "en"),
                TryGet(info.city?.names, "en"),
                info.location?.latitude ?? 0, info.location?.longitude ?? 0);

            lock (cache)
            {
                cache[ip] = geoInfo;
            }
            return(geoInfo);
        }
        private GeoInfo HandleResult(uint ip, string result)
        {
            var ser  = new DataContractJsonSerializer(typeof(NekudoGeopIpFullResponse));
            var info = (NekudoGeopIpFullResponse)ser.ReadObject(new MemoryStream(Encoding.UTF8.GetBytes(result)));

            if (!info.success)
            {
                if (info.error?.code == 104)
                {
                    this.usageExceeded = DateTime.UtcNow.Date;
                }
                lock (cache)
                    this.cache.Remove(ip);
                return(null);
            }

            var geoInfo = new GeoInfo(info.country_code, info.country_name, info.region_code, info.region_name, info.city, info.latitude, info.longitude);

            lock (cache)
            {
                cache[ip] = geoInfo;
            }
            return(geoInfo);
        }