Chapters 2 to 4 described what flood data looks like and how it moves over the API. This chapter covers what happens in between: how a forecasting node turns observations into a flood prediction, how several models are combined into one ensemble, how forecast quality is measured, and how the decision to warn the public is made and carried out. It follows PHASE-3 (Protocol) §6–7 of WIA-ENE-033 and the helper functions of the reference TypeScript SDK.
PHASE-3 defines its scope in one sentence: it covers the protocols "between gauge networks, forecasting nodes, warning publishers, and downstream consumers." These four roles are the backbone of the chapter. They are logical roles, not products. One agency may run all four in a single system, or a national hydrological service may operate the gauge network and forecasting node while a municipal disaster-safety office acts as warning publisher.
FloodAlert.PHASE-3 states that it "reflects the message types" defined in the WMO and OGC documents it references but "does not redefine them." It also does not prescribe a wire-level handshake for each hop. The sequence below is therefore this book's reading of how the ENE-033 objects line up with the roles; it is a practical interpretation, not a normative diagram from the spec.
Gauge network Forecasting node Warning publisher Downstream consumer
| | | |
|-- RiverLevelData --->| | |
|-- PrecipitationData->| | |
|-- DamStatusData ---->| | |
|-- DrainageData ----->| | |
| | run 1D / 2D / AI models | |
| | build EnsemblePrediction| |
| |-- FloodPredictionResult->| |
| | (+ ensemble summary) | |
| | | apply AlertCriteria |
| | | manualReview? -> duty |
| | | officer decides |
| | |-- FloodAlert / CAP 1.2 -->|
| | |<-- delivery receipts -----|
| | | (AlertDistribution.status)
| | | |
|<================ new observations every cycle; the loop repeats =================|
| Hop | Main ENE-033 object(s) | Defined in | What must travel with it |
|---|---|---|---|
| Gauge → forecasting node | RiverLevelData, PrecipitationData, DamStatusData, DrainageData | PHASE-1 (Chapter 2) | Timestamps in UTC, explicit units, station identifiers, the datum of each water level |
| Forecasting node → warning publisher | FloodPredictionResult, EnsemblePrediction | PHASE-1, PHASE-3 §6.3 | Probability, confidence, lead time, the model versions that produced the run |
| Warning publisher → consumer | FloodAlert (emitted as CAP 1.2) | PHASE-3 §7.2 (Chapter 3) | Issuer identity, affected area geometry, onset and expiry, bilingual instructions |
| Consumer → warning publisher | AlertDistribution.status | PHASE-3 §7.3 | Sent, delivered, failed and confirmed counts per channel |
The last row matters more than it seems. A warning chain that never learns whether its messages arrived cannot tell a successful warning from a silent failure. Section 5.8 returns to this.
One practical consequence of splitting the roles is that each hop can fail independently, and each should degrade in a known way. If gauge data stops arriving, the forecasting node should mark its next run as based on stale inputs rather than silently reusing the last value. If the forecasting node is unavailable, the warning publisher still holds its own authority and can issue a warning from observed levels alone. If one distribution channel is down, the others carry on. Writing these fallback rules down, per hop, is part of implementing the protocol even though PHASE-3 does not list them.
For flow along a river channel, PHASE-3 §6.2.1 specifies the one-dimensional Saint-Venant equations. They describe how a flood wave travels and changes shape between gauges, and they are the standard physical basis for river routing.
Continuity: ∂A/∂t + ∂Q/∂x = q
Momentum: ∂Q/∂t + ∂(Q²/A)/∂x + g·A·∂h/∂x + g·A·S_f = 0
| Symbol | Meaning | Unit | Where it comes from in practice |
|---|---|---|---|
| A | Wetted cross-sectional area | m² | Surveyed cross-sections as a function of stage |
| Q | Discharge | m³/s | Model state; compared with RiverLevelData.current.flowRate |
| h | Water surface level | m | Model state; compared with current.waterLevel |
| g | Gravitational acceleration | 9.81 m/s² | Constant |
| Sf | Friction slope | dimensionless | Resistance law, commonly Manning's equation with a calibrated roughness n |
| q | Lateral inflow per unit length | m³/s/m | Tributaries, local runoff, urban drainage outfalls |
| x, t | Distance along the channel, time | m, s | Model grid |
The continuity equation says that water is neither created nor lost: if more water enters a reach than leaves it, the cross-section fills and the level rises. The momentum equation balances the local and convective acceleration of the flow against the pressure gradient (the slope of the water surface) and friction. When the friction slope is written with Manning's law, it becomes Sf = n²Q|Q| / (A²R4/3), where R is the hydraulic radius. The roughness n is the main calibration parameter of most river models.
Many operational systems use simplified forms. Dropping the two acceleration terms gives the diffusive-wave approximation; also dropping the pressure term gives the kinematic wave, in which the water surface is assumed parallel to the bed. The simplifications are cheaper and stable, but they cannot represent backwater — for example a tributary held up by a high main river, or a tidal reach. ENE-033 does not force a particular solution method. The four-point implicit Preissmann scheme is a common choice for the full equations; explicit schemes are simpler but must respect the Courant stability condition, which ties the time step to the grid spacing and wave speed.
The data objects of Chapter 2 feed the 1D model in well-defined places:
DamStatusData.flow.outflow when a dam sits at the head of the reach. On the reading recommended in Chapter 2, outflow is the total release and already includes the spillway flow.discharge, so the two must not be added. The dam's dischargePlan gives the future boundary, which is why dam release plans are a forecast input and not just a status report.PrecipitationData, weighted by soilSaturation.waterLevel before the forecast period starts.RiverLevelData.forecast (peak, peakTime, hourly values).Once water leaves the channel, or when rain ponds in a city faster than drains can remove it, flow is no longer confined to one direction. PHASE-3 §6.2.2 specifies the two-dimensional shallow-water equations for this case.
∂h/∂t + ∂(hu)/∂x + ∂(hv)/∂y = R
∂(hu)/∂t + ∂(hu²)/∂x + ∂(huv)/∂y = −g·h·∂z/∂x − τ_x
∂(hv)/∂t + ∂(huv)/∂x + ∂(hv²)/∂y = −g·h·∂z/∂y − τ_y
Here h is water depth (m), u and v are depth-averaged velocity components (m/s), z is ground elevation (m), R is the rainfall source term (m/s) and τx, τy are bed shear stresses. In the form given in the spec, the stress terms should be read as kinematic stresses (bed shear stress divided by water density), otherwise the units do not match. Readers who implement their own solver should also note that the equation as printed contains only the bed-slope term −gh∂z/∂x. Many solvers write the pressure term separately as ∂(½gh²)/∂x; the two forms are equivalent only when the equation is rearranged consistently, so check which convention a solver uses before comparing results.
The 2D model produces a depth grid for every output time step. PHASE-3 §6.1.1 and the FloodPredictionResult object define what must be derived from it:
| Output field | How it is derived from the depth grid |
|---|---|
inundationArea.estimatedArea (km²) | Sum of cell areas where maximum depth exceeds a wet/dry threshold. The threshold is an implementation choice and must be declared (for example 0.05 m). |
inundationArea.maxDepth (m) | Largest depth over all cells and time steps |
inundationArea.averageDepth (m) | Mean of maximum depth over wet cells |
inundationArea.geoJson | Polygon(s) traced around the wet cells, simplified to a tolerance the consumer can render |
inundationArea.affectedPopulation, affectedBuildings | Overlay of the wet polygon with population and building layers |
Two limits apply to v1.0. The release notes state that inundation models relying on lidar-derived DEMs with horizontal resolution finer than 1 m are out of scope, and that storm-surge coupling is a roadmap item for v1.1. In practice, this means an ENE-033 v1.0 system can model river and urban inundation on a DEM of 1 m or coarser, but a coastal flood driven by surge together with river discharge is not covered by the standard's model definitions. The coastal-flood event type still exists, so a publisher can warn for coastal flooding using an external surge model, but it should state in the alert metadata that the inundation extent did not come from an ENE-033 model.
Resolution interacts with run time. Halving the grid size in two dimensions gives four times as many cells, and for an explicit scheme the Courant condition also halves the time step, so run time grows roughly eightfold. This is why the resolution request field (coarse / medium / fine) matters operationally: a fine run may not finish inside the lead time it is meant to serve.
PHASE-3 §6.1.1 describes a data-driven model in three layers. The input layer takes past and forecast precipitation (the past 24 hours plus the forecast), river levels at key gauges, dam storage and release, terrain (elevation, slope, land use) and drainage facility status. The hidden layers combine an LSTM for temporal patterns, a CNN for spatial patterns, and an attention mechanism that weights the most influential factors. The output layer gives flood probability (0–100 %), the expected inundation extent as GeoJSON, maximum inundation depth and the peak time.
The specification names example values — "ten main gauge points" and "more than 30 years" of historical flood data. These should be read as examples of a reasonable configuration, not as requirements. A small catchment may have only three useful gauges; a newly urbanised basin may have only a decade of representative record.
The training data categories in §6.1.2 are: historical flood events, meteorological data (precipitation, wind, pressure), hydrological data (stage, discharge), terrain data (DEM) and infrastructure data (drainage, levees). Each category brings its own weakness, and three pitfalls deserve explicit treatment in any implementation.
A model learns the relationship between rain and river response that held in its training period. That relationship changes when a basin urbanises (more impervious surface, faster runoff), when a dam or levee is built, when a channel is dredged, or when the rainfall climate itself shifts. A model trained across such a change learns an average of two different rivers. Mitigations include training only on the period after the last major structural change, giving the model explicit inputs that describe the change (for example impervious fraction by year), and re-validating after every known intervention.
Floods above the danger level are rare by definition. In an hourly dataset, the "flood" class may be a tiny fraction of samples. A model optimised for overall error will learn to predict "no flood" almost always and still score well. Mitigations include weighting the loss toward high-flow samples, evaluating on event-based metrics (Section 5.6) rather than overall accuracy, and augmenting the training set with physically simulated extreme events from the 1D/2D models — with care, since the AI model then inherits the physical model's biases.
A hindcast replays the past to test a model. Leakage happens when the hindcast gives the model information it would not have had in real time. The most common cases are: using observed rainfall as the "forecast" input instead of the rainfall forecast actually issued at the time; splitting time series randomly so that adjacent hours of the same event appear in both training and test sets; normalising inputs with statistics computed over the whole record; and using gauge data after quality control and revision, when the live system sees raw data. Each makes the hindcast look better than live operation will be. The safe practice is to split by whole events or by year, and to archive the forecast inputs exactly as they were available at issue time.
An AI model is a member of the ensemble, not a replacement for the physical models. Its strengths — speed, and the ability to learn local responses no physical model captures — are balanced by weak extrapolation beyond the range of its training data, which is exactly where the most dangerous floods lie.
| Model family | Strength | Typical weakness | Main ENE-033 output |
|---|---|---|---|
| 1D Saint-Venant | Physically based routing; handles dam releases and tributaries explicitly | Needs surveyed cross-sections and calibration; no floodplain detail | RiverLevelData.forecast |
| 2D shallow-water | Spatial inundation extent and depth | Computational cost; DEM quality limits accuracy | inundationArea with GeoJSON |
| AI (LSTM + CNN + attention) | Fast; learns local response from data | Weak extrapolation to unseen extremes; sensitive to regime change | Probability, peak time, extent |
| Statistical | Simple, transparent baseline | Cannot represent new conditions | Probability of threshold exceedance |
PHASE-3 §6.3 defines EnsemblePrediction with three members — an AI model, a hydrological model and a statistical model — each returning a full FloodPredictionResult. It then asks for two summaries: an unweighted ensemble block (mean probability, median probability, confidence, uncertainty) and a weighted block (probability, inundation area, peak time) where the weights are based on model performance.
interface EnsemblePrediction {
models: {
ai: FloodPredictionResult;
hydrological: FloodPredictionResult;
statistical: FloodPredictionResult;
};
ensemble: {
meanProbability: number;
medianProbability: number;
confidence: number;
uncertainty: number;
};
weighted: { // performance-weighted mean
probability: number;
inundationArea: number;
peakTime: string;
};
}
Note that EnsemblePrediction, like ModelPerformance, AlertCriteria and AlertDistribution later in this chapter, is defined in the PHASE-3 text but is not exported by the reference SDK's types.ts. Implementations that need these objects on the wire should define them from the spec and expect to reconcile names if a later SDK release adds them.
"Weighted by model performance" leaves the method open. A defensible choice is to derive each member's weight from its verified skill on past events in the same basin and season, using a score that matches what the weight will be applied to:
For example (illustrative numbers only): the three members give flood probabilities of 72 %, 58 % and 40 %, and past verification gives weights of 0.50 (AI), 0.35 (hydrological) and 0.15 (statistical). The unweighted mean is 56.7 % and the median is 58 %, while the weighted probability is 0.50 × 72 + 0.35 × 58 + 0.15 × 40 = 62.3 %. The difference is enough to cross a 60 % threshold, which is why the weighting method must be documented and stable, not tuned from run to run.
Three details need a decision that the spec does not make:
uncertainty — the spec does not define it. A simple, explainable choice is the spread of member probabilities (the standard deviation or the range). State the definition in the forecasting node's documentation.weighted.inundationArea is a single number, while each member carries a full InundationArea object. Average the estimatedArea values, but do not attempt to average GeoJSON polygons; publish the extent from the member or the mapping rule you choose, and say which.weighted.peakTime — averaging timestamps is valid (convert to epoch time, weight, convert back), but when members disagree by many hours, a single average time can fall between two plausible peaks. Report the spread alongside it.Three members is a small ensemble. It captures model structural uncertainty but not the uncertainty of the rainfall forecast itself, which is often the largest source of error beyond a few hours. Where a meteorological service provides ensemble rainfall forecasts, running the hydrological member once per rainfall member gives a much better picture of the range of outcomes. The ENE-033 structure can carry this by treating the hydrological member as the summary of such an inner ensemble.
PHASE-3 §6.1.3 defines ModelPerformance with eight fields:
| Field | Meaning | Unit |
|---|---|---|
accuracy | Share of all forecasts that were correct | % |
precision | Share of predicted floods that occurred | % |
recall | Share of observed floods that were predicted | % |
f1Score | Harmonic mean of precision and recall | score (the spec's target is written as a percentage) |
rmse | Root mean squared error of a continuous variable, typically stage | unit of the variable (m) |
mae | Mean absolute error of the same variable | unit of the variable (m) |
leadTime | Time between forecast issue and event | hours |
falseAlarmRate | Share of false warnings (see the definition problem below) | % |
Categorical metrics come from a 2 × 2 contingency table that compares each forecast of "flood / no flood" (for a defined threshold, location and time window) with what happened.
| Flood observed | No flood observed | |
|---|---|---|
| Flood forecast | a — hits | b — false alarms |
| No flood forecast | c — misses | d — correct negatives |
Probability of detection POD = a / (a + c) (= recall)
False alarm ratio FAR = b / (a + b) (= 1 − precision)
Critical success index CSI = a / (a + b + c)
Accuracy ACC = (a + d) / (a + b + c + d)
F1 score F1 = 2a / (2a + b + c) (= 2·CSI / (1 + CSI))
Probability of false detection POFD = b / (b + d)
There is a naming trap here. In forecast verification, the false alarm ratio (b / (a + b)) is the share of warnings that were false, while the probability of false detection, sometimes also called the false alarm rate, is b / (b + d) — the share of non-events that were wrongly flagged. The two can differ by orders of magnitude for rare events. ENE-033's falseAlarmRate does not say which it means. Implementations should publish the formula next to the number; this book recommends the false alarm ratio, because it is what the public experiences as "the warnings that turned out to be wrong."
Consider an illustrative verification set of 1,000 hourly forecasts at one gauge with 10 flood hours. A model that always says "no flood" scores 99 % accuracy, with POD = 0. A useful model might produce a = 6, b = 14, c = 4, d = 976. Its accuracy is 98.2 % — lower than the useless model — while its POD is 60 %, FAR 70 % and CSI 25 %. Accuracy is dominated by the correct negatives, which are easy and numerous. For this reason event-oriented scores (POD, FAR, CSI) should lead any report, and accuracy should be quoted only with the base rate beside it.
The same example shows why a high POD target and a low false-alarm target pull against each other: warning earlier and more often raises POD and FAR together. Where the balance sits is a policy choice for the warning authority, not a model property.
The unit of verification also has to be fixed before any number is meaningful. Counting per hour, per gauge, per event or per administrative area gives very different values from the same forecasts. An event-based count — did a warning precede this flood by at least the required lead time, and did this warning have a matching flood — is closest to what the public experiences and is recommended for headline reporting, with hourly scores kept for model development.
PHASE-3 lists a targetPerformance of accuracy 85, precision 80, recall 90, F1 85, lead time 6 hours and false alarm rate 15. These are design targets stated by the spec, not measured results of any system. Two cautions apply. First, a precision of 80 % implies a false alarm ratio of 20 %, so a target of 15 % "false alarm rate" is only consistent if it means a different quantity (such as POFD) — another reason to fix the definition. Second, as the example above shows, an 85 % accuracy target says little for a rare-event problem. Any performance claim should state the verification period, the threshold, the number of events, and the formulas used.
RMSE and MAE complement the categorical scores for continuous quantities such as peak stage. RMSE penalises large errors more heavily and is therefore more sensitive to the one badly missed flood; MAE is more robust. Both should be computed on high-flow periods separately from all periods, for the same reason that accuracy misleads.
The warning publisher's first task is to decide whether a prediction becomes a warning. PHASE-3 §7.1 defines AlertCriteria with two parts:
interface AlertCriteria {
automatic: {
riskLevel: RiskLevel; // LEVEL-2 or higher
probability: number; // 60 % or higher
leadTime: number; // minimum lead time (hours)
affectedPopulation: number; // minimum affected population
};
manualReview: {
uncertaintyThreshold: number;
criticalInfrastructure: boolean;
historicalSignificance: boolean;
};
}
The spec does not say how the two parts combine. A reading consistent with the field names, and the one this book recommends, is: issue automatically only when every automatic condition is met and no manualReview trigger fires; route to a duty officer when any trigger fires — the ensemble uncertainty exceeds its threshold, the affected area includes critical infrastructure (a hospital, a substation, a subway entrance), or the event would be historically significant (for example above the largest flood on record). Human review is a safeguard for the cases where automated logic is least reliable, and it keeps the legal authority to warn with the people who hold it.
The 60 % probability and LEVEL-2 thresholds are reference defaults written into the spec, not values validated for any particular basin. Each warning authority should set its own thresholds from local verification and the cost it attaches to misses versus false alarms. Note also that ENE-033's LEVEL scale is not the official Korean flood forecast (홍수주의보 and 홍수경보 issued by the flood control offices); Chapter 1 explains why the two must be crosswalked rather than treated as the same thing.
The reference TypeScript SDK provides four helpers that implement a simple version of this logic.
| Function | Rule in the reference implementation |
|---|---|
calculateRiskScore(p, r, d, dr) | 0.3 × precipitation + 0.4 × river level + 0.2 × dam + 0.1 × drainage (each sub-score 0–100) |
getRiskLevelFromScore(s) | ≥ 80 → LEVEL-4; ≥ 60 → LEVEL-3; ≥ 40 → LEVEL-2; ≥ 20 → LEVEL-1; otherwise LEVEL-0 |
isEvacuationRequired(level, prob) | level ≥ LEVEL-3 and probability ≥ 60 |
estimateTimeToOverflow(current, danger, rise) | (danger − current) / rise; null if rise ≤ 0 or current ≥ danger |
The weights and cut-offs are reference-implementation defaults, not validated thresholds. The SDK also leaves open how each 0–100 sub-score is produced; a river-level sub-score, for instance, could be the current stage expressed as a fraction of the distance between the attention level and the danger level. Whatever mapping is used belongs in the deployment's documentation, because the final risk level depends on it as much as on the weights.
An example with illustrative sub-scores: precipitation 70, river level 85, dam 40, drainage 60 gives 21 + 34 + 8 + 6 = 69, which is LEVEL-3 (Warning). With an ensemble probability of 62.3 %, isEvacuationRequired returns true.
A worked example, adapted from the README sample (which labels the 5.0 m threshold as a warning level; here it is treated as the danger level): the current level is 4.2 m, the danger level 5.0 m and the level is rising at 0.2 m/h. estimateTimeToOverflow returns (5.0 − 4.2) / 0.2 = 4 hours.
This is a linear extrapolation. It assumes the rate of rise stays constant, which is rarely true during the rising limb of a flood. RiverLevelData.trend also carries acceleration (m/h²). With constant acceleration a, the rise after time t is rt + ½at². Taking a = 0.02 m/h² as an example, 0.8 = 0.2t + 0.01t², which solves to about 3.4 hours — more than half an hour earlier than the linear estimate. Both are crude compared with a routed forecast; they are useful as a fallback when the forecasting node is unavailable, and as a sanity check against it. Two further points: the function's parameter is named dangerLevel, while physical overflow happens at bankHeight, so decide and document which threshold "time to overflow" refers to; and the helper returns null both when the river is falling and when it is already above the threshold — callers must check the current level to tell these apart.
RiskLevel is a string enum whose values are 'LEVEL-0' to 'LEVEL-4'. isEvacuationRequired compares them with riskLevel >= RiskLevel.LEVEL_3. In JavaScript this is a lexical string comparison. It gives the right answer only because the five values share a prefix and differ in a single digit. It would silently break if a level such as 'LEVEL-10' were ever added (it sorts before 'LEVEL-2'), if values were localised, or if a value arrived with different case. A numeric mapping removes the dependency:
import { RiskLevel } from './types';
const LEVEL_RANK: Record<RiskLevel, number> = {
[RiskLevel.LEVEL_0]: 0,
[RiskLevel.LEVEL_1]: 1,
[RiskLevel.LEVEL_2]: 2,
[RiskLevel.LEVEL_3]: 3,
[RiskLevel.LEVEL_4]: 4,
};
// Reference-implementation defaults; set per basin from local verification.
const WEIGHTS = { precipitation: 0.3, river: 0.4, dam: 0.2, drainage: 0.1 };
const CUTOFFS: [number, RiskLevel][] = [
[80, RiskLevel.LEVEL_4],
[60, RiskLevel.LEVEL_3],
[40, RiskLevel.LEVEL_2],
[20, RiskLevel.LEVEL_1],
];
export function riskScore(s: { precipitation: number; river: number;
dam: number; drainage: number }): number {
return s.precipitation * WEIGHTS.precipitation + s.river * WEIGHTS.river
+ s.dam * WEIGHTS.dam + s.drainage * WEIGHTS.drainage;
}
export function levelFromScore(score: number): RiskLevel {
for (const [min, level] of CUTOFFS) if (score >= min) return level;
return RiskLevel.LEVEL_0;
}
export function atLeast(level: RiskLevel, min: RiskLevel): boolean {
return LEVEL_RANK[level] >= LEVEL_RANK[min];
}
export function evacuationRequired(level: RiskLevel, probability: number): boolean {
return atLeast(level, RiskLevel.LEVEL_3) && probability >= 60;
}
// Example: riskScore({precipitation:70, river:85, dam:40, drainage:60}) === 69
// levelFromScore(69) === RiskLevel.LEVEL_3
The same atLeast helper should be used for the AlertCriteria.automatic.riskLevel check, so that the automatic-alert rule and the evacuation rule cannot drift apart.
The automatic-alert rule and the evacuation rule both use 60 % probability but at different levels (LEVEL-2 versus LEVEL-3). Keep both values in one configuration source. If an authority raises one and forgets the other, the system can end up recommending evacuation for events it would not even warn about.
Once a FloodAlert is issued, PHASE-3 §7.3 describes its delivery with AlertDistribution. The object has three parts: which channels are used, who the recipients are by priority group, and what happened to the messages.
| Channel flag | Medium | Practical character |
|---|---|---|
sms | Text message to registered numbers | Targeted; needs subscriber lists; queues under load |
push | App push notification | Rich content and links; reaches only app users |
email | Suitable for officials and partner agencies, too slow for the public | |
broadcast | Cell broadcast (the spec labels it 재난 문자, CBS) | Reaches every compatible phone in the target cells without a subscriber list |
siren | Outdoor sirens | Works without phones; carries little information on its own |
radio, tv | Broadcast radio, TV captions | Wide reach; depends on broadcaster cooperation |
social | Social media | Amplification and updates; not a primary channel |
Recipients are grouped as highRisk (residents of high-risk areas), vulnerable (vulnerable groups), firstResponders, publicOfficials and general. The grouping implies an order: responders and officials need the warning early enough to act before the public does, and vulnerable residents — people with limited mobility, care facilities, people who do not read Korean — need both earlier notice and a channel suited to them, which is also why FloodAlert.instructions carries separate ko and en texts.
The status block counts sent, delivered, failed and confirmed. The spec does not state the relationships between them. Sensible invariants to enforce are delivered + failed ≤ sent (the remainder is still pending) and confirmed ≤ delivered. Counts should be kept per channel and per recipient group, not only as a total, since a warning that reached every official but failed on the cell-broadcast channel is a failed public warning. Cell broadcast and sirens usually give no per-recipient receipt, so their delivered counts can only mean "accepted by the network"; record that meaning rather than letting it look like confirmed reception.
Updates and cancellations need the same care as the first message. A warning that is upgraded, extended or withdrawn should reuse the original alertId as its reference so that consumers replace the earlier message instead of showing two conflicting ones; Chapter 3 shows how this maps to CAP 1.2 message types. The README's "within 5 minutes" delivery figure, like the other performance numbers in the spec, is a design target, not a guarantee; measure the actual time from issue to delivery per channel from the status records and report it alongside the verification scores of Section 5.6.
inundationArea and GeoJSON. Sub-1 m lidar DEMs and storm-surge coupling are outside v1.0.EnsemblePrediction combines AI, hydrological and statistical members; weights should come from documented, verified skill, and the undefined uncertainty field needs a declared definition.Chapter 6 turns from protocol to deployment: how these roles are placed on real infrastructure, secured, and connected to national and municipal systems.