Index: /trunk/BNC/CHANGELOG.md
===================================================================
--- /trunk/BNC/CHANGELOG.md	(revision 11136)
+++ /trunk/BNC/CHANGELOG.md	(revision 11137)
@@ -23,4 +23,8 @@
 - ADDED: Option 'File per stream' (key ephFilePerStation) in the RINEX Ephemeris panel: one RINEX navigation file per stream, named after the stream like the observation files, each with all ephemerides of its stream; by default still one file with each ephemeris once
 - ADDED: PPP option 'Ephemeris stream' (key PPP/ephMount): real-time PPP uses the Broadcast Ephemeris of the given stream(s) only, e.g. BCEP00BKG0, not those of the observation streams; empty: all streams as before
+- FIXED: GPS and QZSS uploads within the curve fit interval (cutover; TOC some 16 s earlier than that of the data set replaced, e.g. 01:59:44 instead of 02:00:00) were ignored as not newer until the next regular data set, in RINEX navigation files, ephemeris upload (BCEP), PPP and combination; corrections of ACs referring to such an upload were not used for up to 2 hours. Data sets received before (e.g. from a slower stream) are ignored now explicitly
+- ADDED: GPS/QZSS ephemeris check: data sets with IODE different from the 8 LSBs of IODC (assembled from subframes of two uploads) are rejected
+- FIXED: Ephemeris upload: the periods of validity of the uploaded data sets were not applied (logical or instead of and), so a data set was uploaded as long as no newer one came, e.g. a Galileo data set 32 hours after its TOC
+- CHANGED: SSR QC: the IOD of the corrections is looked up among the last 30 data sets of a satellite instead of the last two (false 'IOD of no received broadcast ephemeris' for Galileo and GLONASS); new report lines for IODs received from the ephemeris stream(s) only later (with the largest delay) and for corrections referring to a broadcast ephemeris outside its validity
 - ADDED: Regression test suite ('tests'): example configurations and short raw captures replayed through BNC and compared with reference outputs
 - ADDED: macOS packaging script
Index: /trunk/BNC/src/PPP/pppEphPool.cpp
===================================================================
--- /trunk/BNC/src/PPP/pppEphPool.cpp	(revision 11136)
+++ /trunk/BNC/src/PPP/pppEphPool.cpp	(revision 11137)
@@ -97,4 +97,11 @@
 /////////////////////////////////////////////////////////////////////////////
 void t_pppEphPool::t_satEphPool::putEphemeris(unsigned maxQueueSize, t_eph* eph) {
+  // data set received before (e.g. from another stream after a newer one)
+  for (unsigned ii = 0; ii < _ephs.size(); ii++) {
+    if (_ephs[ii]->sameDataSet(eph)) {
+      delete eph;
+      return;
+    }
+  }
   if (_ephs.empty() || eph->isNewerThan(_ephs.front())) {
     _ephs.push_front(eph);
Index: /trunk/BNC/src/bncephuser.cpp
===================================================================
--- /trunk/BNC/src/bncephuser.cpp	(revision 11136)
+++ /trunk/BNC/src/bncephuser.cpp	(revision 11137)
@@ -48,5 +48,5 @@
 // Constructor
 ////////////////////////////////////////////////////////////////////////////
-bncEphUser::bncEphUser(bool connectSlots) {
+bncEphUser::bncEphUser(bool connectSlots, unsigned maxQueueSize) : _maxQueueSize(maxQueueSize) {
   if (connectSlots) {
     connect(BNC_CORE, SIGNAL(newGPSEph(t_ephGPS)),
@@ -148,4 +148,15 @@
 
   QString prn(newEph->prn().toInternalString().c_str());
+
+  // data set received before, e.g. from a slower stream after a newer one
+  if (_eph.contains(prn)) {
+    const deque<t_eph*>& qq = _eph[prn];
+    for (unsigned ii = 0; ii < qq.size(); ii++) {
+      if (qq[ii]->sameDataSet(eph)) {
+        delete newEph;
+        return failure;
+      }
+    }
+  }
 
   const t_eph* ephOld = ephLast(prn);
@@ -224,4 +235,12 @@
   if (eph->isUnhealthy()) {
     eph->setCheckState(t_eph::unhealthy);
+    return;
+  }
+
+  // GPS/QZSS: IODE and IODC of the same upload
+  // -------------------------------------------
+  const t_ephGPS* ephGPS = dynamic_cast<const t_ephGPS*>(eph);
+  if (ephGPS && !ephGPS->iodeIodcConsistent()) {
+    eph->setCheckState(t_eph::bad);
     return;
   }
Index: /trunk/BNC/src/bncephuser.h
===================================================================
--- /trunk/BNC/src/bncephuser.h	(revision 11136)
+++ /trunk/BNC/src/bncephuser.h	(revision 11137)
@@ -46,5 +46,5 @@
 
  public:
-  bncEphUser(bool connectSlots);
+  bncEphUser(bool connectSlots, unsigned maxQueueSize = 6);
   virtual ~bncEphUser();
 
@@ -72,4 +72,17 @@
   }
 
+  // latest data set of the satellite with this IOD
+  t_eph* ephWithIOD(const QString& prn, unsigned iod) {
+    if (_eph.contains(prn)) {
+      const std::deque<t_eph*>& qq = _eph[prn];
+      for (int ii = int(qq.size()) - 1; ii >= 0; ii--) {
+        if (qq[ii]->IOD() == iod) {
+          return qq[ii];
+        }
+      }
+    }
+    return 0;
+  }
+
   const QList<QString> prnList() {return _eph.keys();}
 
@@ -80,5 +93,5 @@
   void checkEphemeris(t_eph* eph, bool realTime);
   QMutex                             _mutex;
-  static const unsigned              _maxQueueSize = 6;
+  unsigned                           _maxQueueSize;  // data sets kept per satellite
   QMap<QString, std::deque<t_eph*> > _eph;
 };
Index: /trunk/BNC/src/bnchelp.html
===================================================================
--- /trunk/BNC/src/bnchelp.html	(revision 11136)
+++ /trunk/BNC/src/bnchelp.html	(revision 11137)
@@ -3453,4 +3453,13 @@
     or 'OUTDATED EPHEMERIS' in the logfile.
   </p>
+  <p>
+    A data set is taken as new if its Time of Clock (TOC) is later than that of the latest data set of the
+    satellite. An upload of a GPS or QZSS satellite within the curve fit interval (cutover) gets a TOC slightly
+    earlier than that of the data set it replaces, off the regular grid (e.g. 01:59:44 instead of 02:00:00); such a
+    data set with a different IOD and a TOC up to 10 minutes earlier is taken as new as well, as the satellite
+    broadcasts it from then on and the corrections of the Analysis Centers refer to it. A data set received before,
+    e.g. from a slower stream after a newer one, is ignored. A GPS or QZSS data set whose IODE differs from the 8 least
+    significant bits of its IODC, i.e. assembled from subframes of two uploads, is ignored as wrong.
+  </p>
 
   <p>
@@ -5156,7 +5165,12 @@
     <li>satellites with a healthy, current broadcast ephemeris without orbit corrections, clock corrections, code
       biases or phase biases (the latter two only if the stream provides them for the system),</li>
-    <li>orbit and clock corrections referring to an IOD of no received broadcast ephemeris, to an unhealthy
-      broadcast ephemeris, or to a satellite without broadcast ephemeris (number of corrections in brackets; not
-      checked during the first 2 minutes, while the broadcast ephemerides are received),</li>
+    <li>orbit and clock corrections referring to an IOD of no received broadcast ephemeris, to an IOD received from
+      the ephemeris stream(s) only later (up to 5 minutes, with the largest delay; the Analysis Center switched to a
+      new data set before the ephemeris stream delivered it), to a broadcast ephemeris outside its validity (see section
+      2.16 'Upload Ephemeris' for the periods of validity), to an unhealthy broadcast ephemeris, or to a satellite
+      without broadcast ephemeris (number of corrections in brackets; not checked during the first 2 minutes, while
+      the broadcast ephemerides are received). The IOD is looked up among the last 30 data sets of the satellite,
+      as Galileo e.g. broadcasts a new data set every 10 minutes and Analysis Centers keep one for up to an
+      hour,</li>
     <li>the code bias signals provided, satellites without code biases of the reference signals (default GPS C1W/C2W,
       GLONASS C1P/C2P, Galileo C1C/C5Q, BDS C2I/C6I, QZSS C1C/C2L, SBAS C1C/C5Q; as in the combination, see option
@@ -9784,5 +9798,5 @@
     <li>SBAS ephemeris will be interpreted as outdated and ignored when dt > 600.0 or dt < -600.0.</li>
     <li>QZSS ephemeris will be interpreted as outdated and ignored when dt > 7200.0 or dt < -3600.0.</li>
-    <li>NavIC ephemeris will be interpreted as outdated and ignored when fabs(dt > 86400.0).</li>
+    <li>NavIC ephemeris will be interpreted as outdated and ignored when fabs(dt) > 86400.0.</li>
   </ul>
   A note 'OUTDATED EPHEMERIS' will be given in the logfile and the data will be disregarded when necessary.
Index: /trunk/BNC/src/ephemeris.cpp
===================================================================
--- /trunk/BNC/src/ephemeris.cpp	(revision 11136)
+++ /trunk/BNC/src/ephemeris.cpp	(revision 11137)
@@ -26,4 +26,24 @@
   _clkCorr = 0;
 }
+
+// Is this data set newer than eph (the latest one received)?
+// Usually its TOC is later. An upload of a GPS or QZSS satellite within the
+// curve fit interval (cutover) gets a TOC slightly earlier than that of the
+// data set it replaces (e.g. 01:59:44 instead of 02:00:00, i.e. off the regular
+// grid), and a different IOD; the new upload is broadcast from then on and
+// the corrections of the ACs refer to it.
+////////////////////////////////////////////////////////////////////////////
+bool t_eph::isNewerThan(const t_eph* eph) const {
+  if (eph->_TOC < _TOC) {
+    return true;
+  }
+  const double MAX_CUTOVER_SHIFT = 600.0;  // observed: 16 s, 32 s
+  if ((system() == GPS || system() == QZSS) && system() == eph->system() &&
+      IOD() != eph->IOD() && eph->_TOC - _TOC <= MAX_CUTOVER_SHIFT) {
+    return true;
+  }
+  return false;
+}
+
 // Destructor
 ////////////////////////////////////////////////////////////////////////////
Index: /trunk/BNC/src/ephemeris.h
===================================================================
--- /trunk/BNC/src/ephemeris.h	(revision 11136)
+++ /trunk/BNC/src/ephemeris.h	(revision 11137)
@@ -29,5 +29,6 @@
   virtual int slotNum() const {return 0;}
   bncTime TOC() const {return _TOC;}
-  bool    isNewerThan(const t_eph* eph) const {return earlierTime(eph, this);}
+  bool    isNewerThan(const t_eph* eph) const;
+  bool    sameDataSet(const t_eph* eph) const {return _TOC == eph->_TOC && IOD() == eph->IOD();}
   void    setCheckState(e_checkState checkState) {_checkState = checkState;}
   e_checkState checkState() const {return _checkState;}
@@ -146,4 +147,10 @@
   virtual QString toString(double version) const;
   virtual unsigned int  IOD() const { return static_cast<unsigned int>(_IODE); }
+  // GPS/QZSS: IODE equals the 8 LSBs of IODC, except for data sets assembled
+  // from subframes of two uploads (cutover)
+  bool iodeIodcConsistent() const {
+    return (system() != t_eph::GPS && system() != t_eph::QZSS) ||
+           (static_cast<int>(_IODE) == (static_cast<int>(_IODC) & 0xFF));
+  }
   //virtual unsigned int  isUnhealthy() const { return static_cast<unsigned int>(_health);}
   virtual unsigned int isUnhealthy() const;
Index: /trunk/BNC/src/src.pro
===================================================================
--- /trunk/BNC/src/src.pro	(revision 11136)
+++ /trunk/BNC/src/src.pro	(revision 11137)
@@ -2,6 +2,6 @@
 TARGET = ../bnc
 
-CONFIG -= debug
-CONFIG += release
+CONFIG += debug
+CONFIG -= release
 
 macx {
Index: /trunk/BNC/src/ssrQc.cpp
===================================================================
--- /trunk/BNC/src/ssrQc.cpp	(revision 11136)
+++ /trunk/BNC/src/ssrQc.cpp	(revision 11137)
@@ -56,4 +56,7 @@
   const double MAX_CLK_JUMP   = 0.5;  // [m] clock change beyond its rate between consecutive epochs
   const double EPH_WARMUP     = 120.0; // [s] no ephemeris checks at the start
+  const double EPH_EARLY_MAX  = 300.0; // [s] IOD received this much later still counted as early
+  const double EPH_IOD_REUSE  = 21600.0; // [s] an outdated data set this far from the epoch: IOD reused
+  const unsigned EPH_KEPT     = 30;    // data sets kept per satellite
   const double CMP_SAMPL      = 30.0;  // [s] sampling of the stream comparison
   const double CMP_DELAY      = 60.0;  // [s] epochs compared when data this much newer arrived
@@ -119,5 +122,5 @@
 ////////////////////////////////////////////////////////////////////////////
 t_ssrQc::t_ssrQc(const QString& dirName, double interval, const QStringList& streams,
-                 const QString& reference) : _ephUser(true) {
+                 const QString& reference) : _ephUser(true, EPH_KEPT) {
   _dirName   = dirName;
   _reference = reference.isEmpty() ? QString("CONSENSUS") : reference;
@@ -359,23 +362,69 @@
 // Broadcast ephemeris the correction refers to
 ////////////////////////////////////////////////////////////////////////////
-void t_ssrQc::checkEph(t_satStat& stat, const QString& prnInt, unsigned iod, const bncTime& time,
-                       const string& staID) {
+void t_ssrQc::checkEph(t_satStat& stat, const QString& satKey, const QString& prnInt,
+                       unsigned iod, const bncTime& time, const string& staID) {
   // not before all broadcast ephemerides may have been received
   if (time - _staStart[QString::fromStdString(staID)] < EPH_WARMUP) {
     return;
   }
-  t_eph* ephLast = _ephUser.ephLast(prnInt);
-  t_eph* ephPrev = _ephUser.ephPrev(prnInt);
-  t_eph* eph = 0;
-  if      (ephLast && ephLast->IOD() == iod) eph = ephLast;
-  else if (ephPrev && ephPrev->IOD() == iod) eph = ephPrev;
-  if (!ephLast) {
+  if (!_ephUser.ephLast(prnInt)) {
     stat.noEph += 1;
-  }
-  else if (!eph) {
-    stat.iodUnknown += 1;
+    return;
+  }
+  // any data set kept, not only the last two: Galileo has a new IODnav every
+  // 10 minutes, and ACs keep one for up to an hour
+  t_eph* eph = _ephUser.ephWithIOD(prnInt, iod);
+  // an IOD used again later (GLONASS tb every day, GPS IODE) is another data set
+  if (eph && outDatedBcep(eph, time) && fabs(time - eph->TOC()) > EPH_IOD_REUSE) {
+    eph = 0;
+  }
+  if (!eph) {
+    // received later from the ephemeris stream(s)? decided in resolvePendingIod
+    t_pendingIod& pending = _pendingIod[satKey][iod];
+    if (pending.num == 0) {
+      pending.prnInt = prnInt;
+      pending.first  = currentTime();
+    }
+    pending.num += 1;
   }
   else if (eph->checkState() == t_eph::unhealthy || eph->checkState() == t_eph::bad) {
     stat.ephUnhealthy += 1;
+  }
+  else if (outDatedBcep(eph, time)) {
+    stat.ephOutdated += 1;
+  }
+  if (_pendingIod.contains(satKey)) {
+    resolvePendingIod(satKey, false);
+  }
+}
+
+// Corrections referring to an IOD not received before: counted as early if
+// the ephemeris stream(s) delivered the data set later, as unknown if not
+// within EPH_EARLY_MAX (or at the end of the report interval, final)
+////////////////////////////////////////////////////////////////////////////
+void t_ssrQc::resolvePendingIod(const QString& satKey, bool final) {
+  t_satStat& stat = _satStat[satKey];
+  QMap<unsigned, t_pendingIod>& pend = _pendingIod[satKey];
+  QMutableMapIterator<unsigned, t_pendingIod> it(pend);
+  while (it.hasNext()) {
+    it.next();
+    const t_pendingIod& pending = it.value();
+    t_eph* eph = _ephUser.ephWithIOD(pending.prnInt, it.key());
+    if (eph && eph->receptDateTime().isValid()) {
+      bncTime recept(eph->receptDateTime().toString(Qt::ISODate).toStdString());
+      if (recept >= pending.first) {
+        stat.iodEarly += pending.num;
+        stat.maxEarly  = max(stat.maxEarly, recept - pending.first);
+        it.remove();
+        continue;
+      }
+    }
+    if (final || currentTime() - pending.first > EPH_EARLY_MAX) {
+      stat.iodUnknown += pending.num;
+      it.remove();
+    }
+  }
+  if (pend.isEmpty()) {
+    _pendingIod.remove(satKey);
   }
 }
@@ -395,5 +444,6 @@
     t_satStat& stat = _satStat[QString::fromStdString(corr._staID) + ' ' + prn];
     stat.numOrb += 1;
-    checkEph(stat, QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time, corr._staID);
+    checkEph(stat, QString::fromStdString(corr._staID) + ' ' + prn,
+             QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time, corr._staID);
     double radial = fabs(corr._xr[0]);
     double alongCross = max(fabs(corr._xr[1]), fabs(corr._xr[2]));
@@ -417,5 +467,6 @@
     t_satStat& stat = _satStat[QString::fromStdString(corr._staID) + ' ' + prn];
     stat.numClk += 1;
-    checkEph(stat, QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time, corr._staID);
+    checkEph(stat, QString::fromStdString(corr._staID) + ' ' + prn,
+             QString::fromStdString(corr._prn.toInternalString()), corr._iod, corr._time, corr._staID);
 
     // jump: change between consecutive epochs (same IOD) beyond the clock rate
@@ -837,9 +888,8 @@
       }
       const t_orbCorr& orb = (--itO).value();
-      t_eph* eph = 0;
-      t_eph* ephLast = _ephUser.ephLast(prnInt);
-      t_eph* ephPrev = _ephUser.ephPrev(prnInt);
-      if      (ephLast && ephLast->IOD() == orb._iod) eph = ephLast;
-      else if (ephPrev && ephPrev->IOD() == orb._iod) eph = ephPrev;
+      t_eph* eph = _ephUser.ephWithIOD(prnInt, orb._iod);
+      if (eph && outDatedBcep(eph, tt) && fabs(tt - eph->TOC()) > EPH_IOD_REUSE) {
+        eph = 0;
+      }
       if (!eph || clk._iod != orb._iod) {
         continue;
@@ -1363,4 +1413,10 @@
   bncTime endTime = final ? _lastTime : _intervalStart + _interval;
 
+  // IODs not received: early or unknown (pending ones younger than
+  // EPH_EARLY_MAX are decided in the next interval)
+  for (const QString& satKey : _pendingIod.keys()) {
+    resolvePendingIod(satKey, final);
+  }
+
   // Healthy and current broadcast ephemerides per system
   QMap<char, QSet<QString> > ephSats;
@@ -1475,4 +1531,6 @@
         continue;
       }
+      QMap<QString, unsigned> iodEarly, outdated;
+      double maxEarly = 0.0;
       QMap<QString, unsigned> missOrb, missClk, missCB, missPB, iodUnknown, unhealthy, noEph,
                               jumps, missRef, noYaw, yawPartly;
@@ -1507,4 +1565,9 @@
         const t_satStat& stat = itSat.value();
         if (stat.iodUnknown)   iodUnknown[prn] = stat.iodUnknown;
+        if (stat.iodEarly) {
+          iodEarly[prn] = stat.iodEarly;
+          maxEarly = max(maxEarly, stat.maxEarly);
+        }
+        if (stat.ephOutdated)  outdated[prn]   = stat.ephOutdated;
         if (stat.ephUnhealthy) unhealthy[prn]  = stat.ephUnhealthy;
         if (stat.noEph)        noEph[prn]      = stat.noEph;
@@ -1568,4 +1631,9 @@
       addSats("phase biases missing (healthy broadcast ephemeris available)", missPB);
       addSats("corrections referring to an IOD of no received broadcast ephemeris", iodUnknown);
+      if (!iodEarly.isEmpty()) {
+        addSats(QString("corrections referring to an IOD received from the ephemeris stream(s) "
+                        "only later (up to %1 s)").arg(maxEarly, 0, 'f', 0), iodEarly);
+      }
+      addSats("corrections referring to a broadcast ephemeris outside its validity", outdated);
       addSats("corrections for unhealthy broadcast ephemeris", unhealthy);
       addSats("corrections for satellites without broadcast ephemeris", noEph);
Index: /trunk/BNC/src/ssrQc.h
===================================================================
--- /trunk/BNC/src/ssrQc.h	(revision 11136)
+++ /trunk/BNC/src/ssrQc.h	(revision 11137)
@@ -59,4 +59,7 @@
     unsigned      numClk;
     unsigned      iodUnknown;    // IOD of no received broadcast ephemeris
+    unsigned      iodEarly = 0;  // IOD received from the ephemeris stream(s) only later
+    double        maxEarly = 0.0; // [s] largest delay of these
+    unsigned      ephOutdated = 0; // broadcast ephemeris outside its validity
     unsigned      ephUnhealthy;  // broadcast ephemeris unhealthy/bad
     unsigned      noEph;         // no broadcast ephemeris at all
@@ -77,6 +80,6 @@
   void   newEpoch(const std::string& staID, char sys, e_type type, const bncTime& time,
                   unsigned updateInt, const QString& prn);
-  void   checkEph(t_satStat& stat, const QString& prnInt, unsigned iod, const bncTime& time,
-                  const std::string& staID);
+  void   checkEph(t_satStat& stat, const QString& satKey, const QString& prnInt, unsigned iod,
+                  const bncTime& time, const std::string& staID);
   void   checkInterval(const bncTime& time);
   void   writeReport(bool final);
@@ -138,4 +141,12 @@
   QMap<QString, t_epoStat>  _epoStat;    // key "STA SYS TYPE"
   QMap<QString, t_satStat>  _satStat;    // key "STA PRN"
+  class t_pendingIod {       // IOD not (yet) received, key "STA PRN" and IOD
+   public:
+    QString  prnInt;         // internal PRN (with the Galileo F/NAV, I/NAV flag)
+    bncTime  first;          // reception time of the first correction
+    unsigned num = 0;        // corrections referring to it
+  };
+  QMap<QString, QMap<unsigned, t_pendingIod> > _pendingIod;
+  void   resolvePendingIod(const QString& satKey, bool final);
   QMap<QString, QSet<QString> > _codeSignals;  // key "STA SYS": signals with code biases
   QMap<QString, QSet<QString> > _phaseSignals; // key "STA SYS": signals with phase biases
Index: /trunk/BNC/src/upload/bncephuploadcaster.cpp
===================================================================
--- /trunk/BNC/src/upload/bncephuploadcaster.cpp	(revision 11136)
+++ /trunk/BNC/src/upload/bncephuploadcaster.cpp	(revision 11137)
@@ -92,35 +92,35 @@
 
         if      (ephGPS && ephGPS->system() == t_eph::GPS  &&  (system == "ALL" || system.contains('G'))) {
-          if (dt < 14400.0 || dt > -7200.0) {
+          if (dt < 14400.0 && dt > -7200.0) {
             size = t_ephEncoder::RTCM3(*ephGPS, Array);
           }
         }
         else if (ephGPS && ephGPS->system() == t_eph::QZSS && (system == "ALL" || system.contains('J'))) {
-          if (dt <  7200.0 || dt > -3600.0) {
+          if (dt <  7200.0 && dt > -3600.0) {
             size = t_ephEncoder::RTCM3(*ephGPS, Array);
           }
         }
         else if (ephGlo && (system == "ALL" || system.contains('R'))) {
-          if (dt <  3900.0 || dt > -2100.0) {
+          if (dt <  3900.0 && dt > -2100.0) {
             size = t_ephEncoder::RTCM3(*ephGlo, Array);
           }
         }
         else if (ephGal && (system == "ALL" || system.contains('E'))) {
-          if (dt < 14400.0 || dt >     0.0) {
+          if (dt < 14400.0 && dt >     0.0) {
             size = t_ephEncoder::RTCM3(*ephGal, Array);
           }
         }
         else if (ephSBAS && (system == "ALL" || system.contains('S'))) {
-          if (dt <   600.0 || dt >  -600.0) {
+          if (dt <   600.0 && dt >  -600.0) {
             size = t_ephEncoder::RTCM3(*ephSBAS, Array);
           }
         }
         else if (ephBDS && (system == "ALL" || system.contains('C'))) {
-          if (dt <  3900.0 || dt >     0.0) {
+          if (dt <  3900.0 && dt >     0.0) {
             size = t_ephEncoder::RTCM3(*ephBDS, Array);
           }
         }
         else if (ephGPS && ephGPS->system() == t_eph::NavIC && (system == "ALL" || system.contains('I'))) {
-          if (fabs(dt < 86400.0)) {
+          if (fabs(dt) < 86400.0) {
             size = t_ephEncoder::RTCM3(*ephGPS, Array);
           }
Index: /trunk/BNC/tests/README.md
===================================================================
--- /trunk/BNC/tests/README.md	(revision 11136)
+++ /trunk/BNC/tests/README.md	(revision 11137)
@@ -55,4 +55,8 @@
   (ephemeris messages of BCEP00BKG0, SSRA00EUH0, FFMJ00DEU0, WTZR00DEU0 cut from the
   merged capture): one RINEX 4 navigation file per stream.
+* `replay_eph_cutover`: 02 on `data/eph_cutover_261023_20min.raw` (GPS ephemerides of
+  BCEP00BKG0, BCEP01JPL0, BCEP00CAS0, 23 Oct 2026 01:05-01:25): the G17 upload within the
+  fit interval (IOD 4, TOC 01:59:44, 16 s before IOD 148) is accepted, the data set mixed
+  from both uploads (IODE 4, IODC 148) rejected.
 * `replay_save_sp3_has`: 14 with Galileo HAS (SSRA00EUH0) only; its epochs are
   off the SP3/CLK sampling grid and are written at the preceding grid epoch.
@@ -97,4 +101,9 @@
   `data/combi_multi_261002_5min.raw`. `ssr_qc_bias` adds the CAS DCB file as
   'QC Bias File' (code bias check against the other ACs and the file).
+* `ssr_qc_iod`: IOD check of the SSR QC on `data/qc_iod_261015.raw` (made with
+  `tools/mk_qc_iod.py` from the QC capture of 15 Oct 2026: BCEP00BKG0 with each message only
+  at its first reception from 00:00, SSRA00WHU1/GMV1/CNE1 01:55-02:10): IODs received only
+  later (WHU1), never received (GMV1 G16, upload not in BCEP00BKG0), ephemeris outside its
+  validity (CNE1 GLONASS).
 
 * `decoder_limits`: crafted, CRC-valid frames from `tools/mkcrafted.py`
