Connectivity has quietly become a deal-breaker on Canadian property. Remote work made home internet a hard requirement rather than a nice-to-have. Telecom operators planning where to build next need to know where coverage is already strong and where the gaps are. Proptech platforms get asked, on every listing, whether the place has decent service. All three need the same thing: connectivity as data, attached to the property, at the address level.
Houski carries two connectivity scores on every property, score_internet and score_cell_coverage. This post walks through what they measure, how to pull them for an address, how to find coverage gaps across a region, and how telecom site-selection teams and proptech platforms can use them.
What the connectivity scores measure
Each score is an integer from 0 to 10 on every property, describing the area around it. Higher is always better.
- score_internet. How available and fast home internet is in the area. A high score means fast service is broadly available. A low score flags slow or limited options.
- score_cell_coverage. Quality of cellular coverage in the area. A high score means strong, reliable signal. A low score flags weak or patchy coverage.
The two do not always agree, and the disagreement is the useful part. Wired internet and cellular coverage are built out by different economics. A small town on a fibre route can have excellent home internet and mediocre cellular signal. A stretch of rural highway can have solid cellular coverage from a nearby tower and almost no fixed broadband. Keeping the two scores separate lets you see which kind of connectivity a place actually has.
Because higher is better, the filters read naturally. score_internet_gte=8 returns the well-served end. score_cell_coverage_lte=3 returns the coverage gaps, which for a telecom operator is the part of the map that matters most.
Pulling connectivity for one address
Resolve a street address to a property_id first. This is a live call against the real API, regenerated on every render:
const houski_data = async (): Promise<SearchResponse> => {
// You must copy the SearchResponse type declarations from the
// Houski API documentation to strongly type the response
const url = new URL('https://api.houski.ca/search');
url.searchParams.set('api_key', 'YOUR_API_KEY');
url.searchParams.set('max_results', '1');
url.searchParams.set('query', '121+High+St+W+Moose+Jaw+SK');
const response = await fetch(url);
const data = await response.json();
return data;
}
(async () => {
let data: SearchResponse = await houski_data();
// Log the response
console.log(data);
})();
{
"cache_hit": false,
"cost_cents": 0.019999999552965164,
"data": [
{
"address": "15 121 High Street W",
"property_id": "1b3b5dd01cb5f3a4"
}
],
"error": "",
"match_meta": [
{
"match_value": 1.0,
"property_id": "1b3b5dd01cb5f3a4"
}
],
"price_quote": false,
"result_total": 1,
"time_ms": 463
}
Then pull both connectivity scores, with a couple of context scores, for that property_id:
const houski_data = async (): Promise<PropertiesResponse> => {
// You must copy the PropertiesResponse type declarations from the
// Houski API documentation to strongly type the response
const url = new URL('https://api.houski.ca/properties');
url.searchParams.set('api_key', 'YOUR_API_KEY');
url.searchParams.set('property_id_eq', '4a5bbb5e4772fac6');
url.searchParams.set('select', 'address,city,score_internet,score_cell_coverage,score_quiet,score_nature');
const response = await fetch(url);
const data = await response.json();
return data;
}
(async () => {
let data: PropertiesResponse = await houski_data();
// Log the response
console.log(data);
})();
{
"cache_hit": false,
"cost_cents": 0.429999977350235,
"data": [
{
"address": "24 121 High Street W",
"city": "Moose Jaw",
"property_id": "4a5bbb5e4772fac6",
"score_cell_coverage": 6,
"score_internet": 10,
"score_nature": 4,
"score_quiet": 4
}
],
"error": "",
"pagination": {
"current_page": 1,
"has_next_page": false,
"has_previous_page": false,
"page_total": 1
},
"price_quote": false,
"result_total": 1,
"time_ms": 47,
"ui_info": {
"address": "24 121 High Street W",
"address_link": "ca/sk/moose-jaw/unknown/24-121-high-street-w",
"address_slug": "24-121-high-street-w",
"city": "Moose Jaw",
"city_id": "dadfc732c4225066",
"city_link": "ca/sk/moose-jaw",
"city_slug": "moose_jaw",
"community": "Unknown",
"community_id": "65697f4220d60341",
"community_link": "ca/sk/moose-jaw/unknown",
"community_slug": "unknown",
"country": "Canada",
"country_abbreviation": "CA",
"country_abbreviation_id": "9ace2b6431b7f1be",
"country_abbreviation_link": "ca",
"country_slug": "canada",
"parent_address": "121 High Street W",
"parent_property_id": "b27dc8e23c9fac2d",
"property_id": "4a5bbb5e4772fac6",
"province": "Saskatchewan",
"province_abbreviation": "SK",
"province_abbreviation_id": "9e918fc836441859",
"province_abbreviation_link": "ca/sk",
"province_slug": "saskatchewan"
}
}
This Moose Jaw address shows the split cleanly. Home internet is at the ceiling while cellular coverage sits a step lower, the exact pattern you find in a smaller prairie city that is well wired but not blanketed by towers. A platform that reported a single connectivity number would have hidden that difference.
Set select to only the scores you use.
Finding coverage gaps across a region
The aggregate endpoint returns one number per market, which is how a site-selection team scans a region without pulling every property.
Median home internet for Toronto, a dense, well-served baseline:
const houski_data = async (): Promise<AggregateResponse> => {
// You must copy the AggregateResponse type declarations from the
// Houski API documentation to strongly type the response
const url = new URL('https://api.houski.ca/aggregate');
url.searchParams.set('aggregation', 'median');
url.searchParams.set('api_key', 'YOUR_API_KEY');
url.searchParams.set('city', 'Toronto');
url.searchParams.set('country_abbreviation', 'ca');
url.searchParams.set('field', 'score_internet');
url.searchParams.set('province_abbreviation', 'on');
const response = await fetch(url);
const data = await response.json();
return data;
}
(async () => {
let data: AggregateResponse = await houski_data();
// Log the response
console.log(data);
})();
{
"cache_hit": false,
"cost_cents": 1.0,
"data": [
{
"aggregation": "median",
"field": "score_internet",
"value": "10"
}
],
"error": "",
"price_quote": false,
"time_ms": 52
}
Mean cellular coverage across Toronto:
const houski_data = async (): Promise<AggregateResponse> => {
// You must copy the AggregateResponse type declarations from the
// Houski API documentation to strongly type the response
const url = new URL('https://api.houski.ca/aggregate');
url.searchParams.set('aggregation', 'mean');
url.searchParams.set('api_key', 'YOUR_API_KEY');
url.searchParams.set('city', 'Toronto');
url.searchParams.set('country_abbreviation', 'ca');
url.searchParams.set('field', 'score_cell_coverage');
url.searchParams.set('province_abbreviation', 'on');
const response = await fetch(url);
const data = await response.json();
return data;
}
(async () => {
let data: AggregateResponse = await houski_data();
// Log the response
console.log(data);
})();
{
"cache_hit": false,
"cost_cents": 1.0,
"data": [
{
"aggregation": "mean",
"field": "score_cell_coverage",
"value": "8.686"
}
],
"error": "",
"price_quote": false,
"time_ms": 52
}
Median home internet for a smaller market, to see how far it sits below the metro baseline:
const houski_data = async (): Promise<AggregateResponse> => {
// You must copy the AggregateResponse type declarations from the
// Houski API documentation to strongly type the response
const url = new URL('https://api.houski.ca/aggregate');
url.searchParams.set('aggregation', 'median');
url.searchParams.set('api_key', 'YOUR_API_KEY');
url.searchParams.set('city', 'Moose+Jaw');
url.searchParams.set('country_abbreviation', 'ca');
url.searchParams.set('field', 'score_internet');
url.searchParams.set('province_abbreviation', 'sk');
const response = await fetch(url);
const data = await response.json();
return data;
}
(async () => {
let data: AggregateResponse = await houski_data();
// Log the response
console.log(data);
})();
{
"cache_hit": false,
"cost_cents": 1.0,
"data": [
{
"aggregation": "median",
"field": "score_internet",
"value": "10"
}
],
"error": "",
"price_quote": false,
"time_ms": 43
}
Compare the medians across a list of markets and the gaps fall out of the table. The markets that sit well below the metro baseline on either score are the build-out candidates.
A connectivity comparison across markets
Run both scores as medians across the markets you are evaluating and you get a connectivity profile per place. Each cell is the indicative median score from 0 to 10 with a plain read in brackets so the meaning is obvious: (good), (ok), (bad). Higher is always better, and numbers shift as the model updates:
| Market | Province | Home internet | Cell coverage |
|---|---|---|---|
| Toronto | ON | 9 (good) | 8 (good) |
| Vancouver | BC | 9 (good) | 8 (good) |
| Moose Jaw | SK | 8 (good) | 5 (ok) |
| Smaller towns | various | 5 (ok) | 5 (ok) |
| Rural fringe | various | 3 (bad) | 4 (bad) |
The gradient is the story. Metro cores are saturated on both. Mid-sized cities are usually well wired but thinner on cellular. The rural fringe is where both scores fall, and where the build-out opportunity and the proptech disclosure both live.
How telecom site-selection teams use this
For an operator deciding where to build next, the valuable query is the inverse of the usual one. Instead of finding well-served properties, you find the underserved ones at scale. Filter to low coverage and pull the coordinates so you can count and map the gap. The call below pulls underserved properties across the country, and you narrow it to a province or city by adding province_abbreviation or city to focus on one region:
from dataclasses import dataclass
from dataclasses_json import dataclass_json
import requests
params = {}
params['api_key'] = 'YOUR_API_KEY'
params['country_abbreviation'] = 'ca'
params['results_per_page'] = '5'
params['score_cell_coverage_lte'] = '4'
params['select'] = 'address,city,latitude,longitude,score_internet,score_cell_coverage'
response = requests.get('https://api.houski.ca/properties', params=params)
if response.status_code == 200:
json_data = response.json()
print(json_data)
# You must copy the PropertiesResponse type declarations from the
# Houski API documentation to strongly type the response
typed_response = PropertiesResponse.from_dict(json_data)
# Log the response
print(typed_response)
else:
print(f'Failed to get data: {response}')
{
"cache_hit": false,
"cost_cents": 1.25,
"data": [
{
"address": "6 1681 Sugar Lake Road",
"city": "North Okanagan E",
"latitude": 50.357948303222656,
"longitude": -118.53919219970705,
"property_id": "1002522efece98dd",
"score_cell_coverage": 4,
"score_internet": 2
},
{
"address": "Lot 1 Quadra Island",
"city": "Squamish-Lillooet C",
"latitude": 50.73382568359375,
"longitude": -123.61920928955078,
"property_id": "1005c51855b27f1c",
"score_cell_coverage": 0,
"score_internet": 2
},
{
"address": "A 620 Route 595",
"city": "Southampton",
"latitude": 46.05928039550781,
"longitude": -67.24381256103516,
"property_id": "100a84d7ccc641d1",
"score_cell_coverage": 4,
"score_internet": 9
},
{
"address": "108 Tenpenny Chemin",
"city": "Val-des-Monts",
"latitude": 45.635799407958984,
"longitude": -75.76409149169922,
"property_id": "100c3e82fc260ec7",
"score_cell_coverage": 4,
"score_internet": 9
},
{
"address": "2365 Des Chenes Chemin E",
"city": "La Conception",
"latitude": 46.1617546081543,
"longitude": -74.75040435791016,
"property_id": "10117f1cfb6369a5",
"score_cell_coverage": 4,
"score_internet": 9
}
],
"error": "",
"pagination": {
"current_page": 1,
"has_next_page": true,
"has_previous_page": false,
"page_total": 4354
},
"price_quote": false,
"result_total": 21767,
"time_ms": 25,
"ui_info": {
"country": "Canada",
"country_abbreviation": "CA",
"country_abbreviation_id": "9ace2b6431b7f1be",
"country_abbreviation_link": "ca",
"country_slug": "canada"
}
}
The score_cell_coverage_lte=4 filter restricts the pull to the coverage gap. Pull the coordinates alongside the scores and you can plot the gap on a map, cluster it, and rank candidate tower locations by the number of underserved addresses each would reach.
How proptech platforms use this
For a listings or home-search platform, the two scores answer the connectivity question every remote-work buyer asks, before they ask it. Surface score_internet and score_cell_coverage on the listing the same way you surface walkability or transit. For a buyer whose job depends on a reliable connection, a low internet score is as disqualifying as a bad commute, and showing it up front saves everyone a wasted viewing.
Limitations to be honest about
- Each score describes the area, not the exact address. A single building with poor in-building cellular signal can sit inside a well-covered area.
- They update on a schedule, not in real time. A tower or a fibre build completed last month may not be reflected until the next refresh.
- Each score is a relative ranking, not a speed test. An internet 9 is better served than a 5. It is not a guaranteed download speed.
- The scores reflect availability in the area, built from public, government, and crowdsourced data, not the specific plan a given household has bought.
A full example: sizing a build-out across several towns
The workflow site-selection teams ask about most is sizing a build-out across a list of candidate towns. You have a shortlist of markets and a fixed budget, and you want to rank them by how much underserved demand each holds.
For each town, run a count aggregate filtered to the coverage gap, then compare. To inspect a specific town in detail, pull its underserved properties with coordinates using the call above. To check a single address a field team flagged, resolve it and pull both scores:
from dataclasses import dataclass
from dataclasses_json import dataclass_json
import requests
params = {}
params['api_key'] = 'YOUR_API_KEY'
params['max_results'] = '1'
params['query'] = '121+High+St+W+Moose+Jaw+SK'
response = requests.get('https://api.houski.ca/search', params=params)
if response.status_code == 200:
json_data = response.json()
print(json_data)
# You must copy the SearchResponse type declarations from the
# Houski API documentation to strongly type the response
typed_response = SearchResponse.from_dict(json_data)
# Log the response
print(typed_response)
else:
print(f'Failed to get data: {response}')
{
"cache_hit": true,
"cost_cents": 0.019999999552965164,
"data": [
{
"address": "15 121 High Street W",
"property_id": "1b3b5dd01cb5f3a4"
}
],
"error": "",
"match_meta": [
{
"match_value": 1.0,
"property_id": "1b3b5dd01cb5f3a4"
}
],
"price_quote": false,
"result_total": 1,
"time_ms": 463
}
And the score pull against the returned property_id:
from dataclasses import dataclass
from dataclasses_json import dataclass_json
import requests
params = {}
params['api_key'] = 'YOUR_API_KEY'
params['property_id_eq'] = '4a5bbb5e4772fac6'
params['select'] = 'score_internet,score_cell_coverage'
response = requests.get('https://api.houski.ca/properties', params=params)
if response.status_code == 200:
json_data = response.json()
print(json_data)
# You must copy the PropertiesResponse type declarations from the
# Houski API documentation to strongly type the response
typed_response = PropertiesResponse.from_dict(json_data)
# Log the response
print(typed_response)
else:
print(f'Failed to get data: {response}')
{
"cache_hit": false,
"cost_cents": 0.2200000137090683,
"data": [
{
"address": "24 121 High Street W",
"property_id": "4a5bbb5e4772fac6",
"score_cell_coverage": 6,
"score_internet": 10
}
],
"error": "",
"pagination": {
"current_page": 1,
"has_next_page": false,
"has_previous_page": false,
"page_total": 1
},
"price_quote": false,
"result_total": 1,
"time_ms": 47,
"ui_info": {
"address": "24 121 High Street W",
"address_link": "ca/sk/moose-jaw/unknown/24-121-high-street-w",
"address_slug": "24-121-high-street-w",
"city": "Moose Jaw",
"city_id": "dadfc732c4225066",
"city_link": "ca/sk/moose-jaw",
"city_slug": "moose_jaw",
"community": "Unknown",
"community_id": "65697f4220d60341",
"community_link": "ca/sk/moose-jaw/unknown",
"community_slug": "unknown",
"country": "Canada",
"country_abbreviation": "CA",
"country_abbreviation_id": "9ace2b6431b7f1be",
"country_abbreviation_link": "ca",
"country_slug": "canada",
"parent_address": "121 High Street W",
"parent_property_id": "b27dc8e23c9fac2d",
"property_id": "4a5bbb5e4772fac6",
"province": "Saskatchewan",
"province_abbreviation": "SK",
"province_abbreviation_id": "9e918fc836441859",
"province_abbreviation_link": "ca/sk",
"province_slug": "saskatchewan"
}
}
In your own code, rank the towns by the count of underserved addresses, weight by build cost, and you have a defensible build-out priority list. The same scores that ranked the towns let you map exactly where inside each one the gap concentrates.
What to build next
Whether you are planning a network build-out, surfacing connectivity on listings, or mapping coverage gaps across a province, it starts with one property call or one aggregate call. The fields are documented, the filters compose cleanly, and scanning a whole region for a one-time analysis costs a coffee.
Get started at /api-documentation/quick-start. Pricing starts at $99 a month, and you can have a coverage map running in your terminal in under five minutes.
