selectedAnnual = annual.find((d) => d.survey_year === selectedSurveyYear)
selectedYearMeta = yearMetadata.get(selectedSurveyYear)
html`<div class="dashboard-grid treatment-summary-grid">
<div class="dashboard-card compact">
<h3>Directories parsed</h3>
<p class="dashboard-number"><strong>${status.parsed_years}</strong></p>
<p class="muted">1998, 2000, 2001, and 2003–2021</p>
</div>
<div class="dashboard-card compact">
<h3>Gold review progress</h3>
<p class="dashboard-number"><strong>${status.reviewed_listings}</strong></p>
<p class="muted">Listings reviewed across all years</p>
</div>
<div class="dashboard-card compact">
<h3>Selected U.S. count</h3>
<p class="dashboard-number"><strong>${fmtInt(selectedAnnual?.facility_count)}</strong></p>
<p class="muted">50 states and DC; listing-year count</p>
</div>
<div class="dashboard-card compact">
<h3>Selected-year QA</h3>
<p class="dashboard-status"><strong>${selectedAnnual?.qa_status ?? "Not reviewed"}</strong></p>
<p class="muted">${fmtInt(selectedAnnual?.reviewed_listings)} of ${fmtInt(selectedAnnual?.gold_target)} baseline reviews</p>
</div>
</div>`Historical Treatment Facility Dashboard
Preliminary historical geography and characteristics reconstructed from SAMHSA treatment facility directories.
d3 = await import("https://cdn.jsdelivr.net/npm/d3@7/+esm")
Plot = await import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm")
topojson = await import("https://cdn.jsdelivr.net/npm/topojson-client@3/+esm")
status = await FileAttachment("data/status.json").json()
metadata = await FileAttachment("data/dashboard_metadata.json").json()
annual = await FileAttachment("data/annual_counts.csv").csv({typed: true})
stateCounts = await FileAttachment("data/state_counts.csv").csv({typed: true})
regionCounts = await FileAttachment("data/region_counts.csv").csv({typed: true})
population = await FileAttachment("data/state_population.csv").csv({typed: true})
availability = await FileAttachment("data/attribute_availability.csv").csv({typed: true})
filterCube = await FileAttachment("data/facility_filter_cube.csv").csv({typed: true})
crosswalk = await FileAttachment("data/harmonization_crosswalk.csv").csv({typed: true})
usTopo = await d3.json("https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json")
stateFeatures = topojson.feature(usTopo, usTopo.objects.states).featuresfmtInt = (value) => new Intl.NumberFormat("en-US", {maximumFractionDigits: 0}).format(value ?? 0)
fmtRate = (value) => Number.isFinite(value)
? new Intl.NumberFormat("en-US", {maximumFractionDigits: 1}).format(value)
: "Not available"
fmtChange = (value, suffix = "") => Number.isFinite(value)
? `${value > 0 ? "+" : ""}${new Intl.NumberFormat("en-US", {maximumFractionDigits: 1}).format(value)}${suffix}`
: "Not available"
normalizeMulti = (value) => value == null ? [] : Array.isArray(value) ? value : [value]
splitValues = (value) => new Set(String(value || "").split("|").filter(Boolean))
surveyYears = metadata.years.map((d) => d.survey_year).sort((a, b) => a - b)
yearMetadata = new Map(metadata.years.map((d) => [d.survey_year, d]))
yearLabels = new Map(metadata.years.map((d) => [
d.survey_year,
`${d.survey_year} survey (${d.directory_year} directory)`
]))
populationByYearState = new Map(population.map((d) => [`${d.survey_year}|${d.state}`, d]))
availabilityByKey = new Map(availability.map((d) => [
`${d.survey_year}|${d.characteristic}|${d.value}`,
d.asked === true || String(d.asked).toLowerCase() === "true"
]))
stateNameByFips = new Map(stateFeatures.map((d) => [
String(d.id).padStart(2, "0"),
d.properties.name
]))
stateAbbrByFips = new Map(Object.entries(metadata.state_fips).map(([abbr, fips]) => [fips, abbr]))
stateOptions = Object.keys(metadata.state_fips).sort()
regionOptions = ["Northeast", "Midwest", "South", "West"]
groupOptions = (group) => metadata.filter_groups[group].map((d) => d.value)
groupLabel = (group, value) => metadata.filter_groups[group].find((d) => d.value === value)?.label ?? value
groupBit = (group, value) => metadata.filter_groups[group].find((d) => d.value === value)?.bit ?? 0viewof selectedSurveyYear = Inputs.select(surveyYears, {
label: "Survey year",
value: d3.max(surveyYears),
format: (d) => yearLabels.get(d)
})
viewof scaleMode = Inputs.radio(["Facility count", "Facilities per 100,000"], {
label: "Map and trend scale",
value: "Facility count"
})
viewof selectedOwnership = Inputs.select(groupOptions("ownership"), {
label: "Ownership",
multiple: true,
size: 5,
format: (d) => groupLabel("ownership", d)
})
viewof selectedCenterTypes = Inputs.select(groupOptions("center_type"), {
label: "Center type",
multiple: true,
size: 5,
format: (d) => groupLabel("center_type", d)
})
viewof selectedSettings = Inputs.select(groupOptions("setting"), {
label: "Care setting",
multiple: true,
size: 4,
format: (d) => groupLabel("setting", d)
})
viewof selectedPayments = Inputs.select(groupOptions("payment"), {
label: "Payment accepted/assistance",
multiple: true,
size: 5,
format: (d) => groupLabel("payment", d)
})
viewof selectedServiceFamilies = Inputs.select(groupOptions("service_family"), {
label: "Service families",
multiple: true,
size: 7,
format: (d) => groupLabel("service_family", d)
})
viewof serviceMatchMode = Inputs.radio(["All selected", "Any selected"], {
label: "Multiple service selections",
value: "All selected"
})
rawCodeOptions = availability
.filter((d) => d.survey_year === selectedSurveyYear && d.characteristic === "original_code" && d.asked)
.sort((a, b) => String(a.value).localeCompare(String(b.value)))
rawCodeLabels = new Map(rawCodeOptions.map((d) => [d.value, `${d.value}: ${d.label}`]))
viewof selectedRawCodes = Inputs.select(rawCodeOptions.map((d) => d.value), {
label: "Advanced: original SAMHSA codes",
multiple: true,
size: 7,
format: (d) => rawCodeLabels.get(d) ?? d
})
viewof comparisonStartYear = Inputs.select(surveyYears, {
label: "Start year",
value: d3.min(surveyYears),
format: (d) => yearLabels.get(d)
})
viewof comparisonEndYear = Inputs.select(surveyYears, {
label: "End year",
value: d3.max(surveyYears),
format: (d) => yearLabels.get(d)
})
overlayOptions = [
"US",
...regionOptions.map((d) => `Region:${d}`),
...stateOptions.map((d) => `State:${d}`)
]
overlayLabel = (value) => {
if (value === "US") return "United States"
const [kind, name] = value.split(":")
return kind === "Region" ? `${name} region` : (stateNameByFips.get(metadata.state_fips[name]) ?? name)
}
viewof selectedOverlays = Inputs.select(overlayOptions, {
label: "Trend geographies (maximum five)",
value: ["US"],
multiple: true,
size: 7,
format: overlayLabel
})
viewof changeMetric = Inputs.radio(
["Absolute count change", "Percent change", "Change per 100,000"],
{label: "State change-map metric", value: "Absolute count change"}
)ownershipSelection = normalizeMulti(selectedOwnership)
centerSelection = normalizeMulti(selectedCenterTypes)
settingSelection = normalizeMulti(selectedSettings)
paymentSelection = normalizeMulti(selectedPayments)
serviceSelection = normalizeMulti(selectedServiceFamilies)
rawCodeSelection = normalizeMulti(selectedRawCodes)
overlaySelection = normalizeMulti(selectedOverlays).slice(0, 5)
perCapita = scaleMode === "Facilities per 100,000"
selections = ({
ownership: ownershipSelection,
center_type: centerSelection,
setting: settingSelection,
payment: paymentSelection,
service_family: serviceSelection,
original_code: rawCodeSelection
})
selectionAvailable = (year) => Object.entries(selections).every(([group, values]) =>
values.every((value) => availabilityByKey.get(`${year}|${group}|${value}`) === true)
)
selectedMask = (group, values) => values.reduce((mask, value) => mask | groupBit(group, value), 0)
masks = ({
ownership: selectedMask("ownership", ownershipSelection),
center_type: selectedMask("center_type", centerSelection),
setting: selectedMask("setting", settingSelection),
payment: selectedMask("payment", paymentSelection),
service_family: selectedMask("service_family", serviceSelection)
})
maskPasses = (row, group, values, matchAll = false) => {
if (!values.length) return true
const rowMask = +row[`${group}_mask`] || 0
const wanted = masks[group]
return matchAll ? (rowMask & wanted) === wanted : (rowMask & wanted) !== 0
}
rowPassesHarmonized = (row) => (
maskPasses(row, "ownership", ownershipSelection) &&
maskPasses(row, "center_type", centerSelection) &&
maskPasses(row, "setting", settingSelection) &&
maskPasses(row, "payment", paymentSelection) &&
maskPasses(row, "service_family", serviceSelection, serviceMatchMode === "All selected")
)
rowPassesRawCodes = (row) => {
if (!rawCodeSelection.length) return true
const codes = splitValues(row.service_codes)
return serviceMatchMode === "All selected"
? rawCodeSelection.every((code) => codes.has(code))
: rawCodeSelection.some((code) => codes.has(code))
}
rowPassesShard = (row) => rowPassesHarmonized(row) && rowPassesRawCodes(row)
activeFilterParts = Object.entries(selections)
.filter(([, values]) => values.length)
.map(([group, values]) => {
const labels = group === "original_code"
? values
: values.map((value) => groupLabel(group, value))
return `${group.replaceAll("_", " ")}: ${labels.join(", ")}`
})
activeFilterSummary = activeFilterParts.length
? activeFilterParts.join("; ")
: "All facilities"
Preliminary research visualization
All 22 parsed directories are available, but these are not validated release data. Counts and classifications can change as each directory completes manual gold-sample review. The selected year’s QA status appears throughout the dashboard.
Important
These historical records are not a current treatment locator. SAMHSA warns that pre-2021 N-SSATS and post-2020 N-SUMHSS survey data are not directly trend-comparable.
Plot.plot({
width: 960,
height: 390,
marginLeft: 70,
x: {label: "Survey year", tickFormat: d3.format("d")},
y: {label: "Directory listings", grid: true},
marks: [
Plot.ruleY([0], {stroke: "#cbd5e1"}),
Plot.line(annual, {
x: "survey_year",
y: "facility_count",
stroke: "#1f6f8b",
strokeWidth: 2.5
}),
Plot.dot(annual, {
x: "survey_year",
y: "facility_count",
fill: (d) => d.reviewed_listings > 0 ? "#d97706" : "#1f6f8b",
r: 4,
title: (d) => `${yearLabels.get(d.survey_year)}\nListings: ${fmtInt(d.facility_count)}\nQA: ${d.qa_status}`
}),
Plot.ruleX([2020], {stroke: "#b91c1c", strokeDasharray: "5,4"})
]
})The analytical year is the survey year. The publication year remains in the selector because, for example, the 2000 directory reports the 1999 survey. The red rule marks the N-SSATS/N-SUMHSS transition. Counts are directory listings, not stable cross-year facility entities.
Explore facilities by state
Leave a filter empty to include all values. Selections within ownership, center type, setting, and payment use OR logic; filter groups combine with AND logic.
mapYearRows = await d3.csv(`data/facility_dashboard_${selectedSurveyYear}.csv`, d3.autoType)
mapIsAvailable = selectionAvailable(selectedSurveyYear)
mapFilteredRows = mapIsAvailable ? mapYearRows.filter(rowPassesShard) : []
mapCounts = d3.rollup(mapFilteredRows, (rows) => rows.length, (d) => d.state)
mapPopulationRows = population.filter((d) => d.survey_year === selectedSurveyYear)
mapStateRows = mapPopulationRows.map((pop) => {
const count = mapCounts.get(pop.state) ?? 0
const rate = pop.population > 0 ? count / pop.population * 100000 : NaN
return {...pop, count, rate, value: perCapita ? rate : count}
})
mapByFips = new Map(mapStateRows.map((d) => [String(d.state_fips).padStart(2, "0"), d]))
mapTotal = d3.sum(mapStateRows, (d) => d.count)
mapPopulationTotal = d3.sum(mapStateRows, (d) => d.population)
mapRateTotal = mapPopulationTotal > 0 ? mapTotal / mapPopulationTotal * 100000 : NaNmapIsAvailable
? html`<div class="dashboard-card filter-summary">
<strong>${yearLabels.get(selectedSurveyYear)}</strong>
<span>${activeFilterSummary}</span>
<span><strong>${fmtInt(mapTotal)}</strong> facilities; <strong>${fmtRate(mapRateTotal)}</strong> per 100,000 residents</span>
<span class="qa-badge">${selectedYearMeta.qa_status}: ${selectedYearMeta.reviewed_listings}/${selectedYearMeta.gold_target} baseline reviews</span>
</div>`
: html`<div class="callout callout-style-default callout-warning">
<div class="callout-body-container callout-body">
<strong>Not available for ${yearLabels.get(selectedSurveyYear)}.</strong>
At least one selected characteristic was not asked or cannot be harmonized for this year. This is shown as missing, not zero.
</div>
</div>`Plot.plot({
width: 960,
height: 555,
projection: "albers-usa",
color: {
scheme: "blues",
label: perCapita ? "Facilities per 100,000" : "Facility count",
legend: true,
unknown: "#e5e7eb"
},
marks: [
Plot.geo(stateFeatures, {
fill: (feature) => mapIsAvailable
? mapByFips.get(String(feature.id).padStart(2, "0"))?.value
: NaN,
stroke: "#ffffff",
strokeWidth: 0.8,
title: (feature) => {
const fips = String(feature.id).padStart(2, "0")
const row = mapByFips.get(fips)
if (!mapIsAvailable || !row) return `${feature.properties.name}\nNot available for this selection`
return `${feature.properties.name}
${yearLabels.get(selectedSurveyYear)}
Facilities: ${fmtInt(row.count)}
Rate: ${fmtRate(row.rate)} per 100,000
Population: ${fmtInt(row.population)}
QA: ${selectedYearMeta.qa_status}
Filters: ${activeFilterSummary}`
}
})
]
})Per-capita values use annual Census resident population estimates. Headline totals cover the 50 states and DC. Territories remain in the underlying parser outputs and are reported separately in the Overview metadata.
Compare the current facility selection over time
The filters selected in the State Explorer also govern this tab.
comparisonYearLo = Math.min(comparisonStartYear, comparisonEndYear)
comparisonYearHi = Math.max(comparisonStartYear, comparisonEndYear)
comparisonYears = surveyYears.filter((year) => year >= comparisonYearLo && year <= comparisonYearHi)
comparisonUsesRaw = rawCodeSelection.length > 0
comparisonRows = comparisonUsesRaw
? (await Promise.all(comparisonYears.map((year) =>
d3.csv(`data/facility_dashboard_${year}.csv`, d3.autoType)
))).flat()
: filterCube.filter((d) => d.survey_year >= comparisonYearLo && d.survey_year <= comparisonYearHi)
rowWeight = (row) => comparisonUsesRaw ? 1 : (+row.facility_count || 0)
comparisonPasses = (row) => comparisonUsesRaw ? rowPassesShard(row) : rowPassesHarmonized(row)
geographyRows = (rows, geography) => {
if (geography === "US") return rows
const [kind, value] = geography.split(":")
return kind === "Region"
? rows.filter((d) => d.census_region === value)
: rows.filter((d) => d.state === value)
}
geographyPopulation = (year, geography) => {
const rows = population.filter((d) => d.survey_year === year)
if (geography === "US") return d3.sum(rows, (d) => d.population)
const [kind, value] = geography.split(":")
if (kind === "State") return populationByYearState.get(`${year}|${value}`)?.population ?? NaN
const statesInRegion = new Set(
stateCounts.filter((d) => d.survey_year === year && d.census_region === value).map((d) => d.state)
)
return d3.sum(rows.filter((d) => statesInRegion.has(d.state)), (d) => d.population)
}
trendRows = comparisonYears.flatMap((year) => {
const available = selectionAvailable(year)
const yearRows = available
? comparisonRows.filter((d) => d.survey_year === year && comparisonPasses(d))
: []
return overlaySelection.map((geography) => {
const count = available
? d3.sum(geographyRows(yearRows, geography), rowWeight)
: NaN
const denominator = geographyPopulation(year, geography)
const rate = available && denominator > 0 ? count / denominator * 100000 : NaN
return {
survey_year: year,
geography,
geography_label: overlayLabel(geography),
count,
population: denominator,
rate,
value: perCapita ? rate : count,
available,
qa_status: yearMetadata.get(year)?.qa_status ?? "Not reviewed"
}
})
})
trendYLabel = perCapita ? "Facilities per 100,000 residents" : "Facility count"
trendChart = Plot.plot({
width: 960,
height: 455,
marginLeft: 85,
marginRight: 25,
x: {label: "Survey year", tickFormat: d3.format("d")},
y: {label: trendYLabel, grid: true},
color: {legend: true},
marks: [
Plot.ruleY([0], {stroke: "#cbd5e1"}),
Plot.ruleX([2020], {stroke: "#b91c1c", strokeDasharray: "5,4"}),
Plot.line(trendRows, {
x: "survey_year",
y: "value",
stroke: "geography_label",
strokeWidth: 2.4
}),
Plot.dot(trendRows, {
x: "survey_year",
y: "value",
stroke: "geography_label",
r: 3,
title: (d) => `${d.geography_label}
${yearLabels.get(d.survey_year)}
${trendYLabel}: ${perCapita ? fmtRate(d.value) : fmtInt(d.value)}
Population: ${fmtInt(d.population)}
QA: ${d.qa_status}${d.available ? "" : "\nSelected characteristic not available"}`
})
]
})normalizeMulti(selectedOverlays).length > 5
? html`<div class="callout callout-style-default callout-warning"><div class="callout-body-container callout-body">Only the first five selected geographies are displayed.</div></div>`
: nullsafeFilename = (value) => String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")
wrapCanvasText = (context, text, x, y, maxWidth, lineHeight, maxLines = 3) => {
const words = text.split(/\s+/)
const lines = []
let line = ""
for (const word of words) {
const test = line ? `${line} ${word}` : word
if (context.measureText(test).width > maxWidth && line) {
lines.push(line)
line = word
} else {
line = test
}
}
if (line) lines.push(line)
lines.slice(0, maxLines).forEach((value, index) => context.fillText(value, x, y + index * lineHeight))
}
trendDownloadPng = {
const button = html`<button class="btn btn-primary" type="button">Download trend PNG</button>`
button.onclick = async () => {
const canvas = document.createElement("canvas")
canvas.width = 1600
canvas.height = 900
const context = canvas.getContext("2d")
context.fillStyle = "#ffffff"
context.fillRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "#10253f"
context.font = "bold 36px Arial, sans-serif"
context.fillText("Historical SAMHSA Treatment Facility Listings", 70, 62)
context.font = "22px Arial, sans-serif"
context.fillText(`${comparisonYearLo}–${comparisonYearHi} survey years · ${trendYLabel}`, 70, 100)
context.fillStyle = "#334155"
context.font = "17px Arial, sans-serif"
wrapCanvasText(context, `Filters: ${activeFilterSummary}`, 70, 132, 1460, 22, 2)
const clone = trendChart.cloneNode(true)
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg")
const svgBlob = new Blob([new XMLSerializer().serializeToString(clone)], {type: "image/svg+xml;charset=utf-8"})
const svgUrl = URL.createObjectURL(svgBlob)
const image = new Image()
await new Promise((resolve, reject) => {
image.onload = resolve
image.onerror = reject
image.src = svgUrl
})
context.drawImage(image, 70, 185, 1460, 610)
URL.revokeObjectURL(svgUrl)
context.fillStyle = "#7c2d12"
context.font = "bold 16px Arial, sans-serif"
context.fillText("PRELIMINARY: year-level manual validation is incomplete.", 70, 830)
context.fillStyle = "#475569"
context.font = "14px Arial, sans-serif"
context.fillText("Source: SAMHSA historical treatment facility directories; Census resident population estimates.", 70, 856)
context.fillText(`Generated ${new Date().toISOString().slice(0, 10)} · econdavidhall.com`, 70, 878)
const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"))
const href = URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = href
link.download = `samhsa-facility-trend-${comparisonYearLo}-${comparisonYearHi}-${safeFilename(scaleMode)}.png`
link.click()
setTimeout(() => URL.revokeObjectURL(href), 1000)
}
return button
}
trendDownloadCsv = {
const csv = d3.csvFormat(trendRows.map((d) => ({
survey_year: d.survey_year,
directory_year: yearMetadata.get(d.survey_year)?.directory_year,
geography: d.geography_label,
facility_count: Number.isFinite(d.count) ? d.count : "",
population: Number.isFinite(d.population) ? d.population : "",
facilities_per_100000: Number.isFinite(d.rate) ? d.rate : "",
available: d.available,
qa_status: d.qa_status
})))
const blob = new Blob([csv], {type: "text/csv;charset=utf-8"})
const href = URL.createObjectURL(blob)
return html`<a class="btn btn-outline-primary" href="${href}" download="samhsa-facility-trend-${comparisonYearLo}-${comparisonYearHi}.csv">Download plotted CSV</a>`
}State Change
changeStartAvailable = selectionAvailable(comparisonYearLo)
changeEndAvailable = selectionAvailable(comparisonYearHi)
changeAvailable = changeStartAvailable && changeEndAvailable
yearFilteredStateCounts = (year) => {
if (!selectionAvailable(year)) return new Map()
const rows = comparisonRows.filter((d) => d.survey_year === year && comparisonPasses(d))
return d3.rollup(rows, (values) => d3.sum(values, rowWeight), (d) => d.state)
}
startStateCounts = yearFilteredStateCounts(comparisonYearLo)
endStateCounts = yearFilteredStateCounts(comparisonYearHi)
changeRows = stateOptions.map((state) => {
const start = startStateCounts.get(state) ?? 0
const end = endStateCounts.get(state) ?? 0
const startPopulation = populationByYearState.get(`${comparisonYearLo}|${state}`)?.population ?? NaN
const endPopulation = populationByYearState.get(`${comparisonYearHi}|${state}`)?.population ?? NaN
const startRate = startPopulation > 0 ? start / startPopulation * 100000 : NaN
const endRate = endPopulation > 0 ? end / endPopulation * 100000 : NaN
const absolute = end - start
const percent = start > 0 ? absolute / start * 100 : NaN
const rateChange = endRate - startRate
const value = changeMetric === "Absolute count change"
? absolute
: changeMetric === "Percent change"
? percent
: rateChange
return {
state,
state_fips: metadata.state_fips[state],
start,
end,
startRate,
endRate,
absolute,
percent,
rateChange,
value
}
})
changeByFips = new Map(changeRows.map((d) => [d.state_fips, d]))changeAvailable
? Plot.plot({
width: 960,
height: 555,
projection: "albers-usa",
color: {
type: "diverging",
scheme: "RdBu",
reverse: true,
pivot: 0,
label: changeMetric,
legend: true,
unknown: "#e5e7eb"
},
marks: [
Plot.geo(stateFeatures, {
fill: (feature) => changeByFips.get(String(feature.id).padStart(2, "0"))?.value,
stroke: "#ffffff",
strokeWidth: 0.8,
title: (feature) => {
const row = changeByFips.get(String(feature.id).padStart(2, "0"))
if (!row) return feature.properties.name
return `${feature.properties.name}
${yearLabels.get(comparisonYearLo)}: ${fmtInt(row.start)} (${fmtRate(row.startRate)} per 100,000)
${yearLabels.get(comparisonYearHi)}: ${fmtInt(row.end)} (${fmtRate(row.endRate)} per 100,000)
Count change: ${fmtChange(row.absolute)}
Percent change: ${fmtChange(row.percent, "%")}
Rate change: ${fmtChange(row.rateChange)} per 100,000`
}
})
]
})
: html`<div class="callout callout-style-default callout-warning"><div class="callout-body-container callout-body">
The selected filters are not available in both comparison years, so change is shown as missing rather than zero.
</div></div>`Percent change is unavailable when a state has zero matching facilities in the starting year.
Not yet activated
County assignment still requires Census street geocoding and a minimum 90% high-confidence match rate. ZIP/county fallback assignments will be visibly distinguished when this tab is activated. State geography is available now because it is printed directly in the source directories.
How filters work
- Empty filters include all listings.
- Multiple selections within ownership, center type, setting, and payment use OR logic.
- Different filter groups combine using AND logic.
- Service families and original codes use the displayed All/Any choice.
- A characteristic that was not asked in a year produces a gap or unavailable map, not a zero.
Population and comparability
Rates use annual Census resident population estimates for the 50 states and DC. Survey year drives the charts; directory year remains visible in labels. The 2020 survey / 2021 directory transition is marked because the N-SSATS and N-SUMHSS series are not directly comparable.
Preliminary QA
All 22 PDFs have structurally complete parser outputs.
Manual gold review has begun with the 1998 calibration year.
Every analytical tab retains a preliminary-data warning.
Address-level records, historical phone numbers, and county geography remain outside this preliminary dashboard.